@reactive-skills/runtime 0.1.0
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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.js +114 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +215 -0
- package/dist/core/event-store.d.ts +182 -0
- package/dist/core/event-store.js +762 -0
- package/dist/core/fsm-engine.d.ts +94 -0
- package/dist/core/fsm-engine.js +648 -0
- package/dist/core/guard-evaluator.d.ts +19 -0
- package/dist/core/guard-evaluator.js +103 -0
- package/dist/core/legacy-adapter.d.ts +27 -0
- package/dist/core/legacy-adapter.js +126 -0
- package/dist/core/migration.d.ts +18 -0
- package/dist/core/migration.js +256 -0
- package/dist/core/projection-engine.d.ts +43 -0
- package/dist/core/projection-engine.js +167 -0
- package/dist/core/runtime-hooks.d.ts +51 -0
- package/dist/core/runtime-hooks.js +195 -0
- package/dist/core/types.d.ts +238 -0
- package/dist/core/types.js +55 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +453 -0
- package/dist/sync/cli.d.ts +1 -0
- package/dist/sync/cli.js +186 -0
- package/dist/sync/engine.d.ts +2 -0
- package/dist/sync/engine.js +366 -0
- package/dist/sync/types.d.ts +35 -0
- package/dist/sync/types.js +1 -0
- package/package.json +75 -0
package/dist/sync/cli.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { runSync } from './engine.js';
|
|
4
|
+
function parseArgs(args) {
|
|
5
|
+
const result = {
|
|
6
|
+
targetDirs: [],
|
|
7
|
+
dryRun: false,
|
|
8
|
+
noBackup: false,
|
|
9
|
+
json: false,
|
|
10
|
+
help: false,
|
|
11
|
+
};
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < args.length) {
|
|
14
|
+
const arg = args[i];
|
|
15
|
+
switch (arg) {
|
|
16
|
+
case '--help':
|
|
17
|
+
case '-h':
|
|
18
|
+
result.help = true;
|
|
19
|
+
break;
|
|
20
|
+
case '--dry-run':
|
|
21
|
+
result.dryRun = true;
|
|
22
|
+
break;
|
|
23
|
+
case '--force':
|
|
24
|
+
break;
|
|
25
|
+
case '--no-backup':
|
|
26
|
+
result.noBackup = true;
|
|
27
|
+
break;
|
|
28
|
+
case '--json':
|
|
29
|
+
result.json = true;
|
|
30
|
+
break;
|
|
31
|
+
case '--source':
|
|
32
|
+
case '-s':
|
|
33
|
+
if (i + 1 < args.length) {
|
|
34
|
+
result.sourceDir = path.resolve(args[++i]);
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
case '--target':
|
|
38
|
+
case '-t':
|
|
39
|
+
if (i + 1 < args.length) {
|
|
40
|
+
result.targetDirs.push(path.resolve(args[++i]));
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
case '--skill':
|
|
44
|
+
if (i + 1 < args.length) {
|
|
45
|
+
result.targetSkill = args[++i];
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
if (arg.startsWith('-')) {
|
|
50
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
51
|
+
}
|
|
52
|
+
if (!result.sourceDir) {
|
|
53
|
+
result.sourceDir = path.resolve(arg);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
result.targetDirs.push(path.resolve(arg));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function printHelp() {
|
|
64
|
+
console.log(`
|
|
65
|
+
reactive-skills sync-engine: Locked mirror/PUT skill synchronization
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
reactive-skills sync-engine [source] [targets...] [flags]
|
|
69
|
+
|
|
70
|
+
Arguments:
|
|
71
|
+
source Source skills directory (default: ~/.agents/skills)
|
|
72
|
+
target One or more target directories (default: all known satellites)
|
|
73
|
+
|
|
74
|
+
Flags:
|
|
75
|
+
--source, -s <dir> Source skills directory
|
|
76
|
+
--target, -t <dir> Add a target directory (repeatable)
|
|
77
|
+
--skill <name> Sync a specific skill only
|
|
78
|
+
--dry-run Preview changes without writing
|
|
79
|
+
--mirror Mirror mode (default, only supported mode): destination
|
|
80
|
+
becomes identical to source distributable payload
|
|
81
|
+
--force Accepted but no-op (mirror is the only mode)
|
|
82
|
+
--no-backup Skip timestamped backup before overwrite
|
|
83
|
+
--json Machine-readable JSON output
|
|
84
|
+
--help, -h Show this help
|
|
85
|
+
|
|
86
|
+
Satellites (default targets):
|
|
87
|
+
~/.agents/skills
|
|
88
|
+
~/.claude/skills
|
|
89
|
+
~/.codex/skills
|
|
90
|
+
~/.gemini/config/skills
|
|
91
|
+
~/.pi/skills
|
|
92
|
+
~/.kilocode/skills
|
|
93
|
+
~/.copilot/skills
|
|
94
|
+
~/.hermes/skills
|
|
95
|
+
~/.crew/skills
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
function defaultTargets() {
|
|
99
|
+
const home = os.homedir();
|
|
100
|
+
return [
|
|
101
|
+
path.join(home, '.agents', 'skills'),
|
|
102
|
+
path.join(home, '.claude', 'skills'),
|
|
103
|
+
path.join(home, '.codex', 'skills'),
|
|
104
|
+
path.join(home, '.gemini', 'config', 'skills'),
|
|
105
|
+
path.join(home, '.pi', 'skills'),
|
|
106
|
+
path.join(home, '.kilocode', 'skills'),
|
|
107
|
+
path.join(home, '.copilot', 'skills'),
|
|
108
|
+
path.join(home, '.hermes', 'skills'),
|
|
109
|
+
path.join(home, '.crew', 'skills'),
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
function formatReport(report, json) {
|
|
113
|
+
if (json) {
|
|
114
|
+
return JSON.stringify(report, null, 2);
|
|
115
|
+
}
|
|
116
|
+
const lines = [];
|
|
117
|
+
const prefix = report.dryRun ? '[dry-run] ' : '';
|
|
118
|
+
lines.push(`${prefix}Source: ${report.sourceDir}`);
|
|
119
|
+
lines.push(`${prefix}Skills: ${report.skillsFound} found, ${report.skillsValid} valid, ${report.skillsInvalid} invalid`);
|
|
120
|
+
lines.push(`${prefix}Targets: ${report.targetDirs.length}`);
|
|
121
|
+
lines.push('');
|
|
122
|
+
for (const r of report.results) {
|
|
123
|
+
const icon = r.action === 'mirrored' ? '⇄' :
|
|
124
|
+
r.action === 'unchanged' ? '=' :
|
|
125
|
+
r.action === 'skipped_invalid' ? '!' :
|
|
126
|
+
r.action === 'backed_up' ? '~' :
|
|
127
|
+
r.action === 'skipped_overlap' ? '⊘' : '?';
|
|
128
|
+
const detail = r.reason || r.backupPath || '';
|
|
129
|
+
lines.push(` ${icon} ${r.skill} -> ${path.basename(r.target)} ${detail}`);
|
|
130
|
+
}
|
|
131
|
+
if (report.orphans.length > 0) {
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('Orphans (in dest but not source):');
|
|
134
|
+
for (const o of report.orphans) {
|
|
135
|
+
lines.push(` ${path.basename(o.target)}: ${o.names.join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (report.errors.length > 0) {
|
|
139
|
+
lines.push('');
|
|
140
|
+
lines.push('Errors:');
|
|
141
|
+
for (const e of report.errors) {
|
|
142
|
+
lines.push(` ! ${e}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(`${report.errors.length === 0 ? 'OK' : 'ERRORS'}: ${report.results.length} results, ${report.errors.length} errors`);
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
export async function syncEngineCommand(args) {
|
|
150
|
+
let opts;
|
|
151
|
+
try {
|
|
152
|
+
opts = parseArgs(args);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
return `ERROR: ${err.message}\n\nUse --help for usage.`;
|
|
156
|
+
}
|
|
157
|
+
if (opts.help) {
|
|
158
|
+
printHelp();
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
const sourceDir = opts.sourceDir || path.join(os.homedir(), '.agents', 'skills');
|
|
162
|
+
const targetDirs = opts.targetDirs.length > 0 ? opts.targetDirs : defaultTargets();
|
|
163
|
+
const report = runSync({
|
|
164
|
+
sourceDir,
|
|
165
|
+
targetDirs,
|
|
166
|
+
targetSkill: opts.targetSkill,
|
|
167
|
+
dryRun: opts.dryRun,
|
|
168
|
+
backup: !opts.noBackup,
|
|
169
|
+
});
|
|
170
|
+
return formatReport(report, opts.json);
|
|
171
|
+
}
|
|
172
|
+
const isDirectRun = process.argv[1] &&
|
|
173
|
+
(process.argv[1].endsWith('sync/cli.js') ||
|
|
174
|
+
process.argv[1].endsWith('sync\\cli.js') ||
|
|
175
|
+
process.argv[1].endsWith('sync/cli.ts'));
|
|
176
|
+
if (isDirectRun) {
|
|
177
|
+
syncEngineCommand(process.argv.slice(2))
|
|
178
|
+
.then((output) => {
|
|
179
|
+
if (output)
|
|
180
|
+
console.log(output);
|
|
181
|
+
})
|
|
182
|
+
.catch((err) => {
|
|
183
|
+
console.error(err.message);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
const NEVER_SKILLS = new Set([
|
|
5
|
+
'.git', '.docs', '.reactive', '.playwright-mcp', '.backup', '.sync-backups',
|
|
6
|
+
'tests', 'scripts', 'node_modules', 'dist', 'axi',
|
|
7
|
+
'.cache', '.tmp', '.idea', '.vscode',
|
|
8
|
+
]);
|
|
9
|
+
const EXCLUDE_FROM_SKILL = new Set([
|
|
10
|
+
'.git', '.docs', '.reactive', '.playwright-mcp', '.backup', '.sync-backups',
|
|
11
|
+
'tests', 'scripts', 'node_modules', 'dist',
|
|
12
|
+
]);
|
|
13
|
+
function discoverSkills(sourceDir, targetSkill) {
|
|
14
|
+
if (!fs.existsSync(sourceDir))
|
|
15
|
+
return [];
|
|
16
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
17
|
+
const skills = [];
|
|
18
|
+
for (const e of entries) {
|
|
19
|
+
if (!e.isDirectory())
|
|
20
|
+
continue;
|
|
21
|
+
if (NEVER_SKILLS.has(e.name))
|
|
22
|
+
continue;
|
|
23
|
+
if (targetSkill && e.name !== targetSkill)
|
|
24
|
+
continue;
|
|
25
|
+
const dirPath = path.join(sourceDir, e.name);
|
|
26
|
+
const hasSkillMd = fs.existsSync(path.join(dirPath, 'SKILL.md')) ||
|
|
27
|
+
fs.existsSync(path.join(dirPath, 'skill.md'));
|
|
28
|
+
const hasSkillYaml = fs.existsSync(path.join(dirPath, 'skill.yaml'));
|
|
29
|
+
const isValid = hasSkillMd || hasSkillYaml;
|
|
30
|
+
skills.push({
|
|
31
|
+
name: e.name,
|
|
32
|
+
path: dirPath,
|
|
33
|
+
hasSkillMd,
|
|
34
|
+
hasSkillYaml,
|
|
35
|
+
isValid,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return skills;
|
|
39
|
+
}
|
|
40
|
+
function getDistributableFiles(skillPath) {
|
|
41
|
+
const files = [];
|
|
42
|
+
function walk(dir, rel = '') {
|
|
43
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
44
|
+
for (const e of entries) {
|
|
45
|
+
if (EXCLUDE_FROM_SKILL.has(e.name))
|
|
46
|
+
continue;
|
|
47
|
+
const full = path.join(dir, e.name);
|
|
48
|
+
const relPath = rel ? path.join(rel, e.name) : e.name;
|
|
49
|
+
if (e.isDirectory()) {
|
|
50
|
+
walk(full, relPath);
|
|
51
|
+
}
|
|
52
|
+
else if (e.isFile()) {
|
|
53
|
+
files.push(relPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
walk(skillPath);
|
|
58
|
+
return files.sort();
|
|
59
|
+
}
|
|
60
|
+
function hashFile(filePath) {
|
|
61
|
+
const content = fs.readFileSync(filePath);
|
|
62
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
63
|
+
}
|
|
64
|
+
function getFileHashes(skillPath, files) {
|
|
65
|
+
const hashes = new Map();
|
|
66
|
+
for (const f of files) {
|
|
67
|
+
hashes.set(f, hashFile(path.join(skillPath, f)));
|
|
68
|
+
}
|
|
69
|
+
return hashes;
|
|
70
|
+
}
|
|
71
|
+
function hasExcludedEntryAnywhere(dirPath) {
|
|
72
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
73
|
+
for (const e of entries) {
|
|
74
|
+
if (EXCLUDE_FROM_SKILL.has(e.name))
|
|
75
|
+
return true;
|
|
76
|
+
if (e.isDirectory()) {
|
|
77
|
+
const full = path.join(dirPath, e.name);
|
|
78
|
+
if (hasExcludedEntryAnywhere(full))
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
function getDistributableEntries(skillPath) {
|
|
85
|
+
const entries = [];
|
|
86
|
+
function walk(dir, rel = '') {
|
|
87
|
+
const subdirs = [];
|
|
88
|
+
const files = [];
|
|
89
|
+
const all = fs.readdirSync(dir, { withFileTypes: true });
|
|
90
|
+
for (const e of all) {
|
|
91
|
+
if (EXCLUDE_FROM_SKILL.has(e.name))
|
|
92
|
+
continue;
|
|
93
|
+
const relPath = rel ? path.join(rel, e.name) : e.name;
|
|
94
|
+
if (e.isDirectory()) {
|
|
95
|
+
subdirs.push({ rel: relPath, full: path.join(dir, e.name) });
|
|
96
|
+
}
|
|
97
|
+
else if (e.isFile()) {
|
|
98
|
+
files.push(relPath);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const d of subdirs.sort((a, b) => a.rel.localeCompare(b.rel))) {
|
|
102
|
+
entries.push(d.rel);
|
|
103
|
+
walk(d.full, d.rel);
|
|
104
|
+
}
|
|
105
|
+
for (const f of files.sort()) {
|
|
106
|
+
entries.push(f);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
walk(skillPath);
|
|
110
|
+
return entries;
|
|
111
|
+
}
|
|
112
|
+
function directoriesEqual(srcPath, destPath) {
|
|
113
|
+
const srcEntries = getDistributableEntries(srcPath);
|
|
114
|
+
const destEntries = getDistributableEntries(destPath);
|
|
115
|
+
if (srcEntries.length !== destEntries.length)
|
|
116
|
+
return false;
|
|
117
|
+
for (let i = 0; i < srcEntries.length; i++) {
|
|
118
|
+
if (srcEntries[i] !== destEntries[i])
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const srcFiles = getDistributableFiles(srcPath);
|
|
122
|
+
const destFiles = getDistributableFiles(destPath);
|
|
123
|
+
if (srcFiles.length !== destFiles.length)
|
|
124
|
+
return false;
|
|
125
|
+
for (let i = 0; i < srcFiles.length; i++) {
|
|
126
|
+
if (srcFiles[i] !== destFiles[i])
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const srcHashes = getFileHashes(srcPath, srcFiles);
|
|
130
|
+
const destHashes = getFileHashes(destPath, destFiles);
|
|
131
|
+
for (const [file, hash] of srcHashes) {
|
|
132
|
+
if (destHashes.get(file) !== hash)
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (hasExcludedEntryAnywhere(destPath))
|
|
136
|
+
return false;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
function copyToStaging(srcPath, stagingPath) {
|
|
140
|
+
const entries = fs.readdirSync(srcPath, { withFileTypes: true });
|
|
141
|
+
fs.mkdirSync(stagingPath, { recursive: true });
|
|
142
|
+
for (const e of entries) {
|
|
143
|
+
if (EXCLUDE_FROM_SKILL.has(e.name))
|
|
144
|
+
continue;
|
|
145
|
+
const s = path.join(srcPath, e.name);
|
|
146
|
+
const d = path.join(stagingPath, e.name);
|
|
147
|
+
if (e.isDirectory()) {
|
|
148
|
+
copyToStaging(s, d);
|
|
149
|
+
}
|
|
150
|
+
else if (e.isFile()) {
|
|
151
|
+
fs.copyFileSync(s, d);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function createExternalBackup(targetDir, skillName) {
|
|
156
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
157
|
+
const suffix = crypto.randomBytes(6).toString('hex');
|
|
158
|
+
const backupRoot = path.join(targetDir, '.sync-backups', skillName);
|
|
159
|
+
const backupPath = path.join(backupRoot, `${ts}-${suffix}`);
|
|
160
|
+
fs.mkdirSync(backupPath, { recursive: true });
|
|
161
|
+
const destSkillPath = path.join(targetDir, skillName);
|
|
162
|
+
if (fs.existsSync(destSkillPath)) {
|
|
163
|
+
const entries = fs.readdirSync(destSkillPath, { withFileTypes: true });
|
|
164
|
+
for (const e of entries) {
|
|
165
|
+
const s = path.join(destSkillPath, e.name);
|
|
166
|
+
const d = path.join(backupPath, e.name);
|
|
167
|
+
if (e.isDirectory()) {
|
|
168
|
+
fs.cpSync(s, d, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
else if (e.isFile()) {
|
|
171
|
+
fs.copyFileSync(s, d);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return backupPath;
|
|
176
|
+
}
|
|
177
|
+
function removeDirectoryRecursive(dirPath) {
|
|
178
|
+
if (!fs.existsSync(dirPath))
|
|
179
|
+
return;
|
|
180
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
181
|
+
for (const e of entries) {
|
|
182
|
+
const full = path.join(dirPath, e.name);
|
|
183
|
+
if (e.isDirectory()) {
|
|
184
|
+
removeDirectoryRecursive(full);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
fs.unlinkSync(full);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
fs.rmdirSync(dirPath);
|
|
191
|
+
}
|
|
192
|
+
function atomicSwap(stagingPath, destPath) {
|
|
193
|
+
const destParent = path.dirname(destPath);
|
|
194
|
+
const destName = path.basename(destPath);
|
|
195
|
+
let oldPath;
|
|
196
|
+
try {
|
|
197
|
+
if (fs.existsSync(destPath)) {
|
|
198
|
+
oldPath = path.join(destParent, `.old-${destName}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`);
|
|
199
|
+
fs.renameSync(destPath, oldPath);
|
|
200
|
+
}
|
|
201
|
+
fs.renameSync(stagingPath, destPath);
|
|
202
|
+
if (oldPath && fs.existsSync(oldPath)) {
|
|
203
|
+
removeDirectoryRecursive(oldPath);
|
|
204
|
+
oldPath = undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (oldPath && fs.existsSync(oldPath)) {
|
|
209
|
+
try {
|
|
210
|
+
fs.renameSync(oldPath, destPath);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function pathsOverlap(sourceDir, targetDir) {
|
|
219
|
+
const absSource = path.resolve(sourceDir);
|
|
220
|
+
const absTarget = path.resolve(targetDir);
|
|
221
|
+
const source = process.platform === 'win32' ? absSource.toLowerCase() : absSource;
|
|
222
|
+
const target = process.platform === 'win32' ? absTarget.toLowerCase() : absTarget;
|
|
223
|
+
const sourceToTarget = path.relative(source, target);
|
|
224
|
+
const targetToSource = path.relative(target, source);
|
|
225
|
+
const isSameOrChild = (relative) => relative === '' ||
|
|
226
|
+
(relative !== '..' &&
|
|
227
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
228
|
+
!path.isAbsolute(relative));
|
|
229
|
+
return isSameOrChild(sourceToTarget) || isSameOrChild(targetToSource);
|
|
230
|
+
}
|
|
231
|
+
export function runSync(options) {
|
|
232
|
+
const { sourceDir, targetDirs, targetSkill, dryRun = false, backup = true, } = options;
|
|
233
|
+
const report = {
|
|
234
|
+
dryRun,
|
|
235
|
+
sourceDir,
|
|
236
|
+
targetDirs,
|
|
237
|
+
skillsFound: 0,
|
|
238
|
+
skillsValid: 0,
|
|
239
|
+
skillsInvalid: 0,
|
|
240
|
+
results: [],
|
|
241
|
+
orphans: [],
|
|
242
|
+
errors: [],
|
|
243
|
+
};
|
|
244
|
+
if (!fs.existsSync(sourceDir)) {
|
|
245
|
+
report.errors.push(`Source directory not found: ${sourceDir}`);
|
|
246
|
+
return report;
|
|
247
|
+
}
|
|
248
|
+
const skills = discoverSkills(sourceDir, targetSkill);
|
|
249
|
+
report.skillsFound = skills.length;
|
|
250
|
+
report.skillsValid = skills.filter(s => s.isValid).length;
|
|
251
|
+
report.skillsInvalid = skills.filter(s => !s.isValid).length;
|
|
252
|
+
for (const targetDir of targetDirs) {
|
|
253
|
+
if (pathsOverlap(sourceDir, targetDir)) {
|
|
254
|
+
report.errors.push(`Source and destination overlap: ${sourceDir} -> ${targetDir}`);
|
|
255
|
+
for (const skill of skills) {
|
|
256
|
+
report.results.push({
|
|
257
|
+
skill: skill.name,
|
|
258
|
+
target: targetDir,
|
|
259
|
+
action: 'skipped_overlap',
|
|
260
|
+
reason: 'Source and destination overlap; skipping to prevent data loss',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (!dryRun) {
|
|
266
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
267
|
+
}
|
|
268
|
+
for (const skill of skills) {
|
|
269
|
+
const destPath = path.join(targetDir, skill.name);
|
|
270
|
+
if (!skill.isValid) {
|
|
271
|
+
report.results.push({
|
|
272
|
+
skill: skill.name,
|
|
273
|
+
target: targetDir,
|
|
274
|
+
action: 'skipped_invalid',
|
|
275
|
+
reason: 'Missing SKILL.md or skill.yaml',
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const alreadyIdentical = fs.existsSync(destPath) && directoriesEqual(skill.path, destPath);
|
|
280
|
+
if (alreadyIdentical) {
|
|
281
|
+
report.results.push({
|
|
282
|
+
skill: skill.name,
|
|
283
|
+
target: targetDir,
|
|
284
|
+
action: 'unchanged',
|
|
285
|
+
});
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (dryRun) {
|
|
289
|
+
report.results.push({
|
|
290
|
+
skill: skill.name,
|
|
291
|
+
target: targetDir,
|
|
292
|
+
action: 'mirrored',
|
|
293
|
+
reason: 'dry-run',
|
|
294
|
+
});
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
let backupPath;
|
|
298
|
+
let stagingPath;
|
|
299
|
+
try {
|
|
300
|
+
if (backup && fs.existsSync(destPath)) {
|
|
301
|
+
backupPath = createExternalBackup(targetDir, skill.name);
|
|
302
|
+
report.results.push({
|
|
303
|
+
skill: skill.name,
|
|
304
|
+
target: targetDir,
|
|
305
|
+
action: 'backed_up',
|
|
306
|
+
backupPath,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
stagingPath = path.join(targetDir, `.staging-${skill.name}-${Date.now()}`);
|
|
310
|
+
copyToStaging(skill.path, stagingPath);
|
|
311
|
+
if (!directoriesEqual(skill.path, stagingPath)) {
|
|
312
|
+
throw new Error('Staging validation failed: payload does not match source');
|
|
313
|
+
}
|
|
314
|
+
atomicSwap(stagingPath, destPath);
|
|
315
|
+
stagingPath = undefined;
|
|
316
|
+
report.results.push({
|
|
317
|
+
skill: skill.name,
|
|
318
|
+
target: targetDir,
|
|
319
|
+
action: 'mirrored',
|
|
320
|
+
backupPath,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
if (stagingPath && fs.existsSync(stagingPath)) {
|
|
325
|
+
removeDirectoryRecursive(stagingPath);
|
|
326
|
+
}
|
|
327
|
+
stagingPath = undefined;
|
|
328
|
+
// atomicSwap restores the previous destination on failure, so the
|
|
329
|
+
// destination should already be intact. If it is missing (e.g. the
|
|
330
|
+
// rename-back also failed), fall back to the external backup so the
|
|
331
|
+
// destination is never lost.
|
|
332
|
+
if (backupPath && !fs.existsSync(destPath)) {
|
|
333
|
+
const destSkillPath = path.join(targetDir, skill.name);
|
|
334
|
+
const entries = fs.readdirSync(backupPath, { withFileTypes: true });
|
|
335
|
+
fs.mkdirSync(destSkillPath, { recursive: true });
|
|
336
|
+
for (const e of entries) {
|
|
337
|
+
const s = path.join(backupPath, e.name);
|
|
338
|
+
const d = path.join(destSkillPath, e.name);
|
|
339
|
+
if (e.isDirectory()) {
|
|
340
|
+
fs.cpSync(s, d, { recursive: true });
|
|
341
|
+
}
|
|
342
|
+
else if (e.isFile()) {
|
|
343
|
+
fs.copyFileSync(s, d);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
report.errors.push(`Mirror failed for ${skill.name} to ${targetDir}: ${err.message}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (fs.existsSync(targetDir)) {
|
|
351
|
+
const destEntries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
352
|
+
const destNames = destEntries
|
|
353
|
+
.filter(e => e.isDirectory() &&
|
|
354
|
+
!e.name.startsWith('.staging') &&
|
|
355
|
+
!e.name.startsWith('.old-') &&
|
|
356
|
+
e.name !== '.sync-backups')
|
|
357
|
+
.map(e => e.name);
|
|
358
|
+
const sourceNames = new Set(skills.map(s => s.name));
|
|
359
|
+
const orphans = destNames.filter(n => !sourceNames.has(n));
|
|
360
|
+
if (orphans.length > 0) {
|
|
361
|
+
report.orphans.push({ target: targetDir, names: orphans });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return report;
|
|
366
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface SkillEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
hasSkillMd: boolean;
|
|
5
|
+
hasSkillYaml: boolean;
|
|
6
|
+
isValid: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SyncOptions {
|
|
9
|
+
sourceDir: string;
|
|
10
|
+
targetDirs: string[];
|
|
11
|
+
targetSkill?: string;
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
backup?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface SyncResult {
|
|
16
|
+
skill: string;
|
|
17
|
+
target: string;
|
|
18
|
+
action: 'mirrored' | 'unchanged' | 'skipped_invalid' | 'backed_up' | 'skipped_overlap';
|
|
19
|
+
backupPath?: string;
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface SyncReport {
|
|
23
|
+
dryRun: boolean;
|
|
24
|
+
sourceDir: string;
|
|
25
|
+
targetDirs: string[];
|
|
26
|
+
skillsFound: number;
|
|
27
|
+
skillsValid: number;
|
|
28
|
+
skillsInvalid: number;
|
|
29
|
+
results: SyncResult[];
|
|
30
|
+
orphans: {
|
|
31
|
+
target: string;
|
|
32
|
+
names: string[];
|
|
33
|
+
}[];
|
|
34
|
+
errors: string[];
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reactive-skills/runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reactive Skills Architecture (RSA) core runtime — FSM engine, event store, guard evaluator, projection engine, MCP server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./sync": "./dist/sync/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"reactive-skills-dev": "./dist/cli/dev.js",
|
|
13
|
+
"reactive-skills-sync": "./dist/sync/cli.js",
|
|
14
|
+
"reactive-skills-mcp": "./dist/mcp/server.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"agent",
|
|
22
|
+
"skills",
|
|
23
|
+
"fsm",
|
|
24
|
+
"hsm",
|
|
25
|
+
"event-driven",
|
|
26
|
+
"event-sourcing",
|
|
27
|
+
"reactive"
|
|
28
|
+
],
|
|
29
|
+
"author": "",
|
|
30
|
+
"license": "AGPL-3.0-only",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/Reactive-Skills/reactive-skills.git",
|
|
34
|
+
"directory": "packages/runtime"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.5.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
44
|
+
"handlebars": "^4.7.8",
|
|
45
|
+
"js-yaml": "^4.1.0",
|
|
46
|
+
"tslib": "^2.3.0",
|
|
47
|
+
"zod": "^3.23.8"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@swc-node/register": "~1.11.1",
|
|
51
|
+
"@swc/cli": "~0.8.1",
|
|
52
|
+
"@swc/core": "~1.15.5",
|
|
53
|
+
"@swc/helpers": "~0.5.18",
|
|
54
|
+
"@types/handlebars": "^4.1.0",
|
|
55
|
+
"@types/js-yaml": "^4.0.9",
|
|
56
|
+
"@types/node": "^22.13.0",
|
|
57
|
+
"tsx": "^4.19.2",
|
|
58
|
+
"typescript": "^5.7.3",
|
|
59
|
+
"vitest": "^3.0.4"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/Reactive-Skills/reactive-skills/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/Reactive-Skills/reactive-skills#readme",
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsc",
|
|
67
|
+
"test": "vitest run --config vitest.config.ts && npm run check:docs && npm run check:prose",
|
|
68
|
+
"check:docs": "node scripts/check-docs.js",
|
|
69
|
+
"check:prose": "tsx scripts/check-prose.ts",
|
|
70
|
+
"bump:patch": "node scripts/bump-version.js patch",
|
|
71
|
+
"bump:minor": "node scripts/bump-version.js minor",
|
|
72
|
+
"bump:major": "node scripts/bump-version.js major",
|
|
73
|
+
"demo": "tsx examples/simulate-tdd.ts"
|
|
74
|
+
}
|
|
75
|
+
}
|