repofence 0.1.8 → 0.2.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 +30 -12
- package/dist/cli.js +16 -9
- package/dist/commands/auth.js +2 -2
- package/dist/commands/init.js +69 -28
- package/dist/commands/reverse.js +17 -3
- package/dist/commands/status.js +25 -12
- package/dist/core/api-client.js +3 -3
- package/dist/core/auth.js +1 -1
- package/dist/core/pack-manager.js +169 -61
- package/dist/core/targets.js +40 -0
- package/dist/templates/CLAUDE.md +27 -0
- package/dist/templates/CLAUDE_RULES_SDD.md +20 -0
- package/dist/templates/IDE_CLAUDE.md +33 -0
- package/package.json +3 -6
package/README.md
CHANGED
|
@@ -14,13 +14,15 @@ npm install -g repofence
|
|
|
14
14
|
# 1. Purchase a license and get your token
|
|
15
15
|
repofence auth --web
|
|
16
16
|
|
|
17
|
-
# 2. Initialize spec files + install Cursor
|
|
17
|
+
# 2. Initialize spec files + install assistant assets (Cursor by default)
|
|
18
18
|
repofence init
|
|
19
19
|
|
|
20
20
|
# 3. Auto-populate specs from your codebase
|
|
21
21
|
repofence reverse
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Brownfield tip: run `repofence reverse` first after `init` so your spec layer reflects the current codebase before any AI-driven changes.
|
|
25
|
+
|
|
24
26
|
## Commands
|
|
25
27
|
|
|
26
28
|
### `repofence auth`
|
|
@@ -39,12 +41,14 @@ Get your license at [repofence.com](https://repofence.com).
|
|
|
39
41
|
|
|
40
42
|
---
|
|
41
43
|
|
|
42
|
-
### `repofence init`
|
|
44
|
+
### `repofence init [ide]`
|
|
43
45
|
|
|
44
|
-
Initialize spec files in the current project and install
|
|
46
|
+
Initialize spec files in the current project and install assistant assets.
|
|
45
47
|
|
|
46
48
|
```bash
|
|
47
|
-
repofence init
|
|
49
|
+
repofence init # defaults to Cursor
|
|
50
|
+
repofence init cursor
|
|
51
|
+
repofence init claude
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
**Requires**: a valid token saved via `repofence auth`.
|
|
@@ -57,9 +61,11 @@ Creates in your project:
|
|
|
57
61
|
| `spec/CORE_FLOWS.md` | Critical user and system flows |
|
|
58
62
|
| `spec/BOUNDARIES.md` | Architectural boundaries and constraints |
|
|
59
63
|
| `spec/AI_RULES.md` | Rules for AI agents working on the codebase |
|
|
60
|
-
| `spec/IDE_CURSOR.md` |
|
|
64
|
+
| `spec/IDE_CURSOR.md` or `spec/IDE_CLAUDE.md` | Assistant-specific integration guide |
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
Install targets:
|
|
67
|
+
- `cursor`: installs `/repofence.*` command files in `~/.cursor/commands/`
|
|
68
|
+
- `claude`: installs repofence skills in `~/.claude/skills/` and creates project files `CLAUDE.md` + `.claude/rules/sdd-core.md`
|
|
63
69
|
|
|
64
70
|
**Options**:
|
|
65
71
|
|
|
@@ -89,6 +95,8 @@ Detects:
|
|
|
89
95
|
|
|
90
96
|
Updates `spec/SYSTEM_OVERVIEW.md`, `spec/CORE_FLOWS.md`, `spec/BOUNDARIES.md`, and `spec/AI_RULES.md` with what it finds, preserving sections you've already customized.
|
|
91
97
|
|
|
98
|
+
For brownfield projects this is the most important command to run early.
|
|
99
|
+
|
|
92
100
|
**Options**:
|
|
93
101
|
|
|
94
102
|
| Flag | Description |
|
|
@@ -99,12 +107,14 @@ Updates `spec/SYSTEM_OVERVIEW.md`, `spec/CORE_FLOWS.md`, `spec/BOUNDARIES.md`, a
|
|
|
99
107
|
|
|
100
108
|
---
|
|
101
109
|
|
|
102
|
-
### `repofence status`
|
|
110
|
+
### `repofence status [ide]`
|
|
103
111
|
|
|
104
|
-
Show the status of your local token and installed pack.
|
|
112
|
+
Show the status of your local token and installed pack manifests.
|
|
105
113
|
|
|
106
114
|
```bash
|
|
107
|
-
repofence status
|
|
115
|
+
repofence status # shows cursor + claude
|
|
116
|
+
repofence status cursor
|
|
117
|
+
repofence status claude
|
|
108
118
|
```
|
|
109
119
|
|
|
110
120
|
---
|
|
@@ -113,11 +123,12 @@ repofence status
|
|
|
113
123
|
|
|
114
124
|
```
|
|
115
125
|
repofence auth --web # one-time: purchase license + get token
|
|
116
|
-
repofence init
|
|
117
|
-
repofence reverse #
|
|
126
|
+
repofence init claude # or: repofence init cursor
|
|
127
|
+
repofence reverse # critical for brownfield: auto-fill specs from code
|
|
118
128
|
```
|
|
119
129
|
|
|
120
|
-
After running `init`, open
|
|
130
|
+
After running `init cursor`, open Cursor and type `/repofence.help`.
|
|
131
|
+
After running `init claude`, open Claude Code and type `/repofence-reverse`.
|
|
121
132
|
|
|
122
133
|
## Global Options
|
|
123
134
|
|
|
@@ -144,6 +155,12 @@ Deploy (e.g. **Railway**): see [`backend/README.md`](backend/README.md) and root
|
|
|
144
155
|
|
|
145
156
|
Step-by-step: **[docs/RELEASE.md](docs/RELEASE.md)** (`npm version`, `npm publish`). The published package only includes **`dist/`**; the HTTP API in `backend/` is deployed separately.
|
|
146
157
|
|
|
158
|
+
## Claude rollout docs
|
|
159
|
+
|
|
160
|
+
- [docs/CLAUDE-ASSET-CONTRACT.md](docs/CLAUDE-ASSET-CONTRACT.md)
|
|
161
|
+
- [docs/CLAUDE-SKILL-MAPPING.md](docs/CLAUDE-SKILL-MAPPING.md)
|
|
162
|
+
- [docs/CLAUDE-GA-CHECKLIST.md](docs/CLAUDE-GA-CHECKLIST.md)
|
|
163
|
+
|
|
147
164
|
## Environment variables
|
|
148
165
|
|
|
149
166
|
| Variable | Description |
|
|
@@ -151,6 +168,7 @@ Step-by-step: **[docs/RELEASE.md](docs/RELEASE.md)** (`npm version`, `npm publis
|
|
|
151
168
|
| `REPOFENCE_API_BASE_URL` | API host for the CLI (`auth`, `init`). Default normalizes to `…/api`. |
|
|
152
169
|
| `REPOFENCE_MOCK` | Set to `1` for mock API responses (no network). |
|
|
153
170
|
| `REPOFENCE_PACK_ID` | Pack id for `repofence init` (default: `core`). |
|
|
171
|
+
| `REPOFENCE_CLAUDE_PACK_ID` | Optional pack id override for `repofence init claude` (falls back to `REPOFENCE_PACK_ID`). |
|
|
154
172
|
| `REPOFENCE_PUBLIC_KEY` | Optional. PEM or base64 public key to verify signed packs. If unset, the CLI uses its built-in public key. |
|
|
155
173
|
|
|
156
174
|
## Requirements
|
package/dist/cli.js
CHANGED
|
@@ -56,6 +56,7 @@ const init_1 = require("./commands/init");
|
|
|
56
56
|
const reverse_1 = require("./commands/reverse");
|
|
57
57
|
const auth_1 = require("./commands/auth");
|
|
58
58
|
const status_1 = require("./commands/status");
|
|
59
|
+
const targets_1 = require("./core/targets");
|
|
59
60
|
const program = new commander_1.Command();
|
|
60
61
|
program
|
|
61
62
|
.name('repofence')
|
|
@@ -69,18 +70,19 @@ program
|
|
|
69
70
|
program
|
|
70
71
|
.command('init')
|
|
71
72
|
.description('Initialize spec files in the current directory')
|
|
72
|
-
.argument('[ide]', 'IDE
|
|
73
|
+
.argument('[ide]', 'IDE target: cursor | claude', 'cursor')
|
|
73
74
|
.option('-f, --force', 'overwrite existing files')
|
|
74
75
|
.option('--name <name>', 'override project name detection')
|
|
75
76
|
.action(async (ide = 'cursor', options) => {
|
|
76
|
-
if (
|
|
77
|
-
console.error(chalk_1.default.red(`❌ Error: Unsupported IDE "${ide}".
|
|
77
|
+
if (!(0, targets_1.isIdeTarget)(ide)) {
|
|
78
|
+
console.error(chalk_1.default.red(`❌ Error: Unsupported IDE "${ide}". Supported: "cursor", "claude".`));
|
|
78
79
|
process.exit(1);
|
|
79
80
|
}
|
|
80
81
|
const cwd = process.cwd();
|
|
81
82
|
const globalOpts = program.opts();
|
|
82
83
|
try {
|
|
83
84
|
await (0, init_1.initCommand)(cwd, {
|
|
85
|
+
ide,
|
|
84
86
|
force: options.force || false,
|
|
85
87
|
dryRun: globalOpts.dryRun || false,
|
|
86
88
|
verbose: globalOpts.verbose || false,
|
|
@@ -117,9 +119,9 @@ program
|
|
|
117
119
|
// Auth command
|
|
118
120
|
program
|
|
119
121
|
.command('auth')
|
|
120
|
-
.description('
|
|
121
|
-
.option('--token <token>', 'Token
|
|
122
|
-
.option('--web', '
|
|
122
|
+
.description('Authenticate the CLI with a token from repofence.com')
|
|
123
|
+
.option('--token <token>', 'Token from repofence.com (after purchase)')
|
|
124
|
+
.option('--web', 'Open login/checkout in the browser to get a token')
|
|
123
125
|
.action(async (options) => {
|
|
124
126
|
const cwd = process.cwd();
|
|
125
127
|
try {
|
|
@@ -134,11 +136,16 @@ program
|
|
|
134
136
|
// Status command
|
|
135
137
|
program
|
|
136
138
|
.command('status')
|
|
137
|
-
.description('Show local token status and installed pack manifest')
|
|
138
|
-
.
|
|
139
|
+
.description('Show local token status and installed pack manifest(s)')
|
|
140
|
+
.argument('[ide]', 'Status target: cursor | claude | all', 'all')
|
|
141
|
+
.action(async (ide = 'all') => {
|
|
142
|
+
if (ide !== 'all' && !(0, targets_1.isIdeTarget)(ide)) {
|
|
143
|
+
console.error(chalk_1.default.red(`❌ Error: Unsupported status target "${ide}". Supported: "cursor", "claude", "all".`));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
139
146
|
const cwd = process.cwd();
|
|
140
147
|
try {
|
|
141
|
-
await (0, status_1.statusCommand)(cwd);
|
|
148
|
+
await (0, status_1.statusCommand)(cwd, { ide });
|
|
142
149
|
}
|
|
143
150
|
catch (error) {
|
|
144
151
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
package/dist/commands/auth.js
CHANGED
|
@@ -36,10 +36,10 @@ const authCommand = async (_cwd, options) => {
|
|
|
36
36
|
}
|
|
37
37
|
const token = options.token ?? (await promptToken());
|
|
38
38
|
if (!token) {
|
|
39
|
-
throw new Error('No
|
|
39
|
+
throw new Error('No token provided; try again with --token <value>.');
|
|
40
40
|
}
|
|
41
41
|
await (0, auth_1.saveToken)(token);
|
|
42
|
-
console.log(chalk_1.default.green(`✅ Token
|
|
42
|
+
console.log(chalk_1.default.green(`✅ Token saved to ${auth_1.tokenPath}`));
|
|
43
43
|
};
|
|
44
44
|
exports.authCommand = authCommand;
|
|
45
45
|
//# sourceMappingURL=auth.js.map
|
package/dist/commands/init.js
CHANGED
|
@@ -39,21 +39,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.initCommand = initCommand;
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
-
const os = __importStar(require("os"));
|
|
43
42
|
const chalk_1 = __importDefault(require("chalk"));
|
|
44
43
|
const templates_1 = require("../core/templates");
|
|
45
44
|
const api_client_1 = require("../core/api-client");
|
|
46
45
|
const auth_1 = require("../core/auth");
|
|
47
46
|
const pack_manager_1 = require("../core/pack-manager");
|
|
48
|
-
const
|
|
47
|
+
const targets_1 = require("../core/targets");
|
|
48
|
+
const BASE_SPEC_FILES = [
|
|
49
49
|
'SYSTEM_OVERVIEW.md',
|
|
50
50
|
'CORE_FLOWS.md',
|
|
51
51
|
'BOUNDARIES.md',
|
|
52
52
|
'AI_RULES.md',
|
|
53
|
-
|
|
53
|
+
];
|
|
54
|
+
const CLAUDE_PROJECT_FILES = [
|
|
55
|
+
{ relPath: 'CLAUDE.md', template: 'CLAUDE.md' },
|
|
56
|
+
{ relPath: path.join('.claude', 'rules', 'sdd-core.md'), template: 'CLAUDE_RULES_SDD.md' },
|
|
54
57
|
];
|
|
55
58
|
async function initCommand(cwd, options) {
|
|
56
|
-
const { force = false, dryRun = false, verbose = false, name } = options;
|
|
59
|
+
const { ide, force = false, dryRun = false, verbose = false, name } = options;
|
|
60
|
+
const profile = (0, targets_1.getTargetProfile)(ide);
|
|
61
|
+
const specFiles = [...BASE_SPEC_FILES, profile.specGuideTemplate];
|
|
57
62
|
// Show banner
|
|
58
63
|
console.log('\n' + chalk_1.default.bold('='.repeat(60)));
|
|
59
64
|
console.log(chalk_1.default.bold('Repofence CLI (packs)'));
|
|
@@ -68,7 +73,7 @@ async function initCommand(cwd, options) {
|
|
|
68
73
|
console.log(`📦 Project name: ${projectName}`);
|
|
69
74
|
}
|
|
70
75
|
console.log(`Project: ${cwd}`);
|
|
71
|
-
console.log(`Installing for:
|
|
76
|
+
console.log(`Installing for: ${profile.displayName}\n`);
|
|
72
77
|
// Create spec directory
|
|
73
78
|
const specDir = path.join(cwd, 'spec');
|
|
74
79
|
if (!dryRun) {
|
|
@@ -90,7 +95,7 @@ async function initCommand(cwd, options) {
|
|
|
90
95
|
// Process each template file
|
|
91
96
|
const createdFiles = [];
|
|
92
97
|
const skippedFiles = [];
|
|
93
|
-
for (const fileName of
|
|
98
|
+
for (const fileName of specFiles) {
|
|
94
99
|
const filePath = path.join(specDir, fileName);
|
|
95
100
|
const fileExists = fs.existsSync(filePath);
|
|
96
101
|
if (fileExists && !force) {
|
|
@@ -124,9 +129,33 @@ async function initCommand(cwd, options) {
|
|
|
124
129
|
process.exit(1);
|
|
125
130
|
}
|
|
126
131
|
}
|
|
132
|
+
// Claude-only project-level files
|
|
133
|
+
if (ide === 'claude') {
|
|
134
|
+
for (const file of CLAUDE_PROJECT_FILES) {
|
|
135
|
+
const filePath = path.join(cwd, file.relPath);
|
|
136
|
+
const fileExists = fs.existsSync(filePath);
|
|
137
|
+
if (fileExists && !force) {
|
|
138
|
+
skippedFiles.push(file.relPath);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const template = (0, templates_1.loadTemplate)(file.template);
|
|
142
|
+
const rendered = (0, templates_1.renderTemplate)(template, context);
|
|
143
|
+
if (dryRun) {
|
|
144
|
+
console.log(`\n[DRY RUN] Would create: ${filePath}`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
148
|
+
fs.writeFileSync(filePath, rendered, 'utf-8');
|
|
149
|
+
createdFiles.push(file.relPath);
|
|
150
|
+
if (verbose) {
|
|
151
|
+
console.log(chalk_1.default.green(`✓ Created: ${file.relPath}`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
127
156
|
// Download and install repofence pack
|
|
128
157
|
let packInstalled = false;
|
|
129
|
-
const packId =
|
|
158
|
+
const packId = (0, targets_1.getPackIdForTarget)(ide);
|
|
130
159
|
if (!dryRun) {
|
|
131
160
|
const token = await (0, auth_1.readToken)();
|
|
132
161
|
if (!token) {
|
|
@@ -156,40 +185,38 @@ async function initCommand(cwd, options) {
|
|
|
156
185
|
}
|
|
157
186
|
catch (error) {
|
|
158
187
|
const message = error instanceof Error ? error.message : String(error);
|
|
159
|
-
console.error(chalk_1.default.red(`❌
|
|
188
|
+
console.error(chalk_1.default.red(`❌ Failed to download pack "${packId}" (${resolvedBase}): ${message}`));
|
|
160
189
|
throw error;
|
|
161
190
|
}
|
|
162
191
|
const isValid = await (0, pack_manager_1.validateSignature)(pack);
|
|
163
192
|
if (!isValid) {
|
|
164
|
-
throw new Error('
|
|
193
|
+
throw new Error('Invalid pack signature.');
|
|
165
194
|
}
|
|
166
|
-
await (0, pack_manager_1.installPack)(pack, { force, verbose });
|
|
195
|
+
await (0, pack_manager_1.installPack)(pack, { force, verbose, target: ide }, profile.installBaseDir);
|
|
167
196
|
packInstalled = true;
|
|
168
197
|
if (verbose) {
|
|
169
|
-
console.log(chalk_1.default.green(`✓ Pack "${pack.packId}"
|
|
198
|
+
console.log(chalk_1.default.green(`✓ Pack "${pack.packId}" installed`));
|
|
170
199
|
}
|
|
171
200
|
}
|
|
172
201
|
else {
|
|
173
|
-
|
|
174
|
-
console.log(`[DRY RUN] Would download pack "${packId}" and install into: ${path.join(homeDir, '.cursor/commands/')}`);
|
|
202
|
+
console.log(`[DRY RUN] Would download pack "${packId}" and install into: ${profile.installBaseDir}`);
|
|
175
203
|
}
|
|
176
204
|
// Print summary
|
|
177
205
|
if (!dryRun) {
|
|
178
|
-
printSummary(createdFiles, skippedFiles, force, packInstalled);
|
|
206
|
+
printSummary(createdFiles, skippedFiles, force, packInstalled, ide);
|
|
179
207
|
}
|
|
180
208
|
else {
|
|
181
209
|
console.log(`\n[DRY RUN] Would create ${createdFiles.length} file(s), skip ${skippedFiles.length} file(s) and install pack "${packId}"`);
|
|
182
210
|
}
|
|
183
211
|
}
|
|
184
|
-
function printSummary(createdFiles, skippedFiles, force, packInstalled) {
|
|
185
|
-
const
|
|
186
|
-
const cursorDir = path.join(homeDir, '.cursor');
|
|
212
|
+
function printSummary(createdFiles, skippedFiles, force, packInstalled, ide) {
|
|
213
|
+
const profile = (0, targets_1.getTargetProfile)(ide);
|
|
187
214
|
console.log('\n' + chalk_1.default.bold('='.repeat(60)));
|
|
188
215
|
console.log(chalk_1.default.green.bold('✨ Installation successful!'));
|
|
189
216
|
console.log(chalk_1.default.bold('='.repeat(60)));
|
|
190
217
|
if (packInstalled) {
|
|
191
218
|
console.log('\n📊 Repofence pack:');
|
|
192
|
-
console.log(` • Installed at: ${
|
|
219
|
+
console.log(` • Installed at: ${profile.installBaseDir}`);
|
|
193
220
|
}
|
|
194
221
|
console.log('\n' + chalk_1.default.dim('─'.repeat(60)));
|
|
195
222
|
console.log(chalk_1.default.bold('What was installed:'));
|
|
@@ -203,21 +230,35 @@ function printSummary(createdFiles, skippedFiles, force, packInstalled) {
|
|
|
203
230
|
});
|
|
204
231
|
}
|
|
205
232
|
// Pack installation
|
|
206
|
-
if (packInstalled) {
|
|
233
|
+
if (packInstalled && ide === 'cursor') {
|
|
207
234
|
console.log('\n Cursor (GLOBAL at ~/.cursor/):');
|
|
208
|
-
console.log(
|
|
235
|
+
console.log(' • ~/.cursor/commands/ repofence commands');
|
|
236
|
+
}
|
|
237
|
+
if (packInstalled && ide === 'claude') {
|
|
238
|
+
console.log('\n Claude Code (GLOBAL at ~/.claude/):');
|
|
239
|
+
console.log(' • ~/.claude/skills/ repofence skills');
|
|
209
240
|
}
|
|
210
241
|
console.log('\n' + chalk_1.default.dim('─'.repeat(60)));
|
|
211
242
|
console.log(chalk_1.default.cyan.bold('Next Steps - Read Carefully!'));
|
|
212
243
|
console.log(chalk_1.default.dim('─'.repeat(60)));
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
244
|
+
if (ide === 'cursor') {
|
|
245
|
+
console.log(chalk_1.default.green('\n✓ Installation complete. The /repofence.* commands are available in Cursor.\n'));
|
|
246
|
+
console.log(chalk_1.default.yellow.bold('IMPORTANT:') + ' These commands run ' + chalk_1.default.green.bold('INSIDE CURSOR'));
|
|
247
|
+
console.log(' ' + chalk_1.default.dim('(not in this terminal)') + '\n');
|
|
248
|
+
console.log(' 1. Open any project in Cursor');
|
|
249
|
+
console.log(' 2. Start a chat with the AI assistant');
|
|
250
|
+
console.log(' 3. In the chat window, type: ' + chalk_1.default.cyan('/repofence.help'));
|
|
251
|
+
console.log(' 4. Use pack commands such as: ' + chalk_1.default.cyan('/repofence.read-specs'));
|
|
252
|
+
console.log(chalk_1.default.yellow('\nRemember:') + ' /repofence.* commands are used in the Cursor chat, not in bash.');
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
console.log(chalk_1.default.green('\n✓ Installation complete. The repofence skills are available in Claude Code.\n'));
|
|
256
|
+
console.log(chalk_1.default.yellow.bold('IMPORTANT:') + ' These skills run ' + chalk_1.default.green.bold('INSIDE CLAUDE CODE'));
|
|
257
|
+
console.log(' ' + chalk_1.default.dim('(not in this terminal)') + '\n');
|
|
258
|
+
console.log(' 1. Open Claude Code in any project');
|
|
259
|
+
console.log(' 2. Type: ' + chalk_1.default.cyan('/repofence-reverse'));
|
|
260
|
+
console.log(' 3. Continue with: ' + chalk_1.default.cyan('/repofence-plan') + ' and ' + chalk_1.default.cyan('/repofence-build'));
|
|
261
|
+
}
|
|
221
262
|
console.log(chalk_1.default.dim('─'.repeat(60)) + '\n');
|
|
222
263
|
}
|
|
223
264
|
//# sourceMappingURL=init.js.map
|
package/dist/commands/reverse.js
CHANGED
|
@@ -142,13 +142,27 @@ async function reverseCommand(cwd, options) {
|
|
|
142
142
|
}
|
|
143
143
|
// Print summary
|
|
144
144
|
if (!dryRun) {
|
|
145
|
-
printSummary(updatedFiles, skippedFiles, scanResult, force);
|
|
145
|
+
printSummary(updatedFiles, skippedFiles, scanResult, force, specDir);
|
|
146
146
|
}
|
|
147
147
|
else {
|
|
148
148
|
console.log(`\n[DRY RUN] Would update ${updatedFiles.length} file(s), skip ${skippedFiles.length} file(s)`);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
function
|
|
151
|
+
function getIdeGuideLine(specDir) {
|
|
152
|
+
const cursorGuide = fs.existsSync(path.join(specDir, 'IDE_CURSOR.md'));
|
|
153
|
+
const claudeGuide = fs.existsSync(path.join(specDir, 'IDE_CLAUDE.md'));
|
|
154
|
+
if (claudeGuide && !cursorGuide) {
|
|
155
|
+
return ' 4. Use these specs with Claude Code (see /spec/IDE_CLAUDE.md)';
|
|
156
|
+
}
|
|
157
|
+
if (cursorGuide && !claudeGuide) {
|
|
158
|
+
return ' 4. Use these specs with Cursor AI (see /spec/IDE_CURSOR.md)';
|
|
159
|
+
}
|
|
160
|
+
if (claudeGuide && cursorGuide) {
|
|
161
|
+
return ' 4. Use the guide that matches your target: /spec/IDE_CLAUDE.md or /spec/IDE_CURSOR.md';
|
|
162
|
+
}
|
|
163
|
+
return ' 4. Add an IDE guide in /spec/ (IDE_CLAUDE.md or IDE_CURSOR.md)';
|
|
164
|
+
}
|
|
165
|
+
function printSummary(updatedFiles, skippedFiles, scanResult, force, specDir) {
|
|
152
166
|
console.log('\n' + chalk_1.default.bold('='.repeat(60)));
|
|
153
167
|
console.log(chalk_1.default.green.bold('✨ Reverse engineering complete!'));
|
|
154
168
|
console.log(chalk_1.default.bold('='.repeat(60)));
|
|
@@ -177,7 +191,7 @@ function printSummary(updatedFiles, skippedFiles, scanResult, force) {
|
|
|
177
191
|
console.log(' 1. Review the updated spec files in /spec/');
|
|
178
192
|
console.log(' 2. Fill in any UNKNOWN sections with actual details');
|
|
179
193
|
console.log(' 3. Customize flows and boundaries as needed');
|
|
180
|
-
console.log(
|
|
194
|
+
console.log(getIdeGuideLine(specDir));
|
|
181
195
|
console.log('');
|
|
182
196
|
}
|
|
183
197
|
//# sourceMappingURL=reverse.js.map
|
package/dist/commands/status.js
CHANGED
|
@@ -7,25 +7,38 @@ exports.statusCommand = void 0;
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const auth_1 = require("../core/auth");
|
|
9
9
|
const pack_manager_1 = require("../core/pack-manager");
|
|
10
|
-
const
|
|
10
|
+
const targets_1 = require("../core/targets");
|
|
11
|
+
const printManifest = async (target) => {
|
|
12
|
+
const profile = (0, targets_1.getTargetProfile)(target);
|
|
13
|
+
const manifest = await (0, pack_manager_1.readManifest)(profile.installBaseDir);
|
|
14
|
+
console.log(chalk_1.default.bold(`\n[${profile.displayName}]`));
|
|
15
|
+
if (!manifest) {
|
|
16
|
+
console.log(chalk_1.default.yellow(`⚠️ No pack manifest found at ${(0, pack_manager_1.manifestFilePath)(profile.installBaseDir)}`));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
console.log(chalk_1.default.cyan(`Pack: ${manifest.pack_id} v${manifest.pack_version}`));
|
|
20
|
+
console.log(chalk_1.default.cyan(`Installed: ${manifest.installed_at}`));
|
|
21
|
+
console.log(chalk_1.default.cyan(`Expires: ${manifest.expires_at ?? 'never'}`));
|
|
22
|
+
console.log(chalk_1.default.cyan(`Artifacts:`));
|
|
23
|
+
manifest.commands.forEach((cmd) => console.log(` - ${cmd}`));
|
|
24
|
+
console.log(chalk_1.default.gray(`Manifest: ${(0, pack_manager_1.manifestFilePath)(profile.installBaseDir)}`));
|
|
25
|
+
};
|
|
26
|
+
const statusCommand = async (cwd, options = {}) => {
|
|
27
|
+
void cwd;
|
|
28
|
+
const target = options.ide || 'all';
|
|
11
29
|
const token = await (0, auth_1.readToken)();
|
|
12
|
-
const manifest = await (0, pack_manager_1.readManifest)();
|
|
13
30
|
if (token) {
|
|
14
|
-
console.log(chalk_1.default.green(`✅ Token
|
|
31
|
+
console.log(chalk_1.default.green(`✅ Token present (${auth_1.tokenPath})`));
|
|
15
32
|
}
|
|
16
33
|
else {
|
|
17
|
-
console.log(chalk_1.default.yellow('⚠️
|
|
34
|
+
console.log(chalk_1.default.yellow('⚠️ No token configured. Run `repofence auth --token <value>`'));
|
|
18
35
|
}
|
|
19
|
-
if (
|
|
20
|
-
|
|
36
|
+
if (target === 'all') {
|
|
37
|
+
await printManifest('cursor');
|
|
38
|
+
await printManifest('claude');
|
|
21
39
|
return;
|
|
22
40
|
}
|
|
23
|
-
|
|
24
|
-
console.log(chalk_1.default.cyan(`Instalado: ${manifest.installed_at}`));
|
|
25
|
-
console.log(chalk_1.default.cyan(`Expira: ${manifest.expires_at ?? 'sin expirar'}`));
|
|
26
|
-
console.log(chalk_1.default.cyan(`Comandos:`));
|
|
27
|
-
manifest.commands.forEach((cmd) => console.log(` - ${cmd}`));
|
|
28
|
-
console.log(chalk_1.default.gray(`Manifest: ${(0, pack_manager_1.manifestFilePath)()}`));
|
|
41
|
+
await printManifest(target);
|
|
29
42
|
};
|
|
30
43
|
exports.statusCommand = statusCommand;
|
|
31
44
|
//# sourceMappingURL=status.js.map
|
package/dist/core/api-client.js
CHANGED
|
@@ -21,16 +21,16 @@ const DEMO_PACK = {
|
|
|
21
21
|
files: [
|
|
22
22
|
{
|
|
23
23
|
path: 'repofence-spec.md',
|
|
24
|
-
contentBase64: Buffer.from('# repofence spec (demo)\n\n_TODO:
|
|
24
|
+
contentBase64: Buffer.from('# repofence spec (demo)\n\n_TODO: pack content_\n').toString('base64'),
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
path: 'repofence-plan.md',
|
|
28
|
-
contentBase64: Buffer.from('# repofence plan (demo)\n\n_TODO:
|
|
28
|
+
contentBase64: Buffer.from('# repofence plan (demo)\n\n_TODO: pack content_\n').toString('base64'),
|
|
29
29
|
},
|
|
30
30
|
],
|
|
31
31
|
};
|
|
32
32
|
const notImplemented = () => {
|
|
33
|
-
throw new Error('API client
|
|
33
|
+
throw new Error('API client not implemented. Set REPOFENCE_MOCK=1 for dummy data.');
|
|
34
34
|
};
|
|
35
35
|
/**
|
|
36
36
|
* Production hosts expose routes under `/api/v1/...`.
|
package/dist/core/auth.js
CHANGED
|
@@ -16,7 +16,7 @@ exports.tokenPath = tokenFile;
|
|
|
16
16
|
const saveToken = async (token) => {
|
|
17
17
|
const normalized = token.trim();
|
|
18
18
|
if (!normalized) {
|
|
19
|
-
throw new Error('
|
|
19
|
+
throw new Error('Empty token; provide a valid token.');
|
|
20
20
|
}
|
|
21
21
|
await ensureDir();
|
|
22
22
|
await promises_1.default.writeFile(tokenFile, normalized, { mode: 0o600 });
|
|
@@ -49,84 +49,226 @@ const sha256Base64 = (buf) => crypto_1.default.createHash('sha256').update(buf).
|
|
|
49
49
|
const downloadBuffer = async (url) => {
|
|
50
50
|
const res = await fetch(url);
|
|
51
51
|
if (!res.ok) {
|
|
52
|
-
throw new Error(`
|
|
52
|
+
throw new Error(`Download failed (${res.status}) ${res.statusText}`);
|
|
53
53
|
}
|
|
54
54
|
const arrayBuffer = await res.arrayBuffer();
|
|
55
55
|
return Buffer.from(arrayBuffer);
|
|
56
56
|
};
|
|
57
|
+
const listFilesRecursive = async (root, relative = '.') => {
|
|
58
|
+
const dir = path_1.default.join(root, relative);
|
|
59
|
+
const entries = await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
60
|
+
const files = [];
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const rel = path_1.default.join(relative, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
files.push(...(await listFilesRecursive(root, rel)));
|
|
65
|
+
}
|
|
66
|
+
else if (entry.isFile()) {
|
|
67
|
+
files.push(rel);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
};
|
|
72
|
+
const toSkillName = (commandPath) => {
|
|
73
|
+
const base = path_1.default.basename(commandPath, '.md');
|
|
74
|
+
if (base.startsWith('repofence.')) {
|
|
75
|
+
return `repofence-${base.slice('repofence.'.length)}`;
|
|
76
|
+
}
|
|
77
|
+
return base.replace(/[._]/g, '-');
|
|
78
|
+
};
|
|
79
|
+
const extractDescription = (markdown) => {
|
|
80
|
+
const match = markdown.match(/\*\*Description\*\*:\s*(.+)/);
|
|
81
|
+
if (match?.[1])
|
|
82
|
+
return match[1].trim();
|
|
83
|
+
const firstSentence = markdown.split('\n').find((line) => line.trim() && !line.startsWith('#'));
|
|
84
|
+
return (firstSentence || 'Repofence workflow skill').replace(/^[-*]\s*/, '').trim();
|
|
85
|
+
};
|
|
86
|
+
const stripCursorCommandHeader = (markdown) => {
|
|
87
|
+
const lines = markdown.split('\n');
|
|
88
|
+
const filtered = [];
|
|
89
|
+
let skipUntilAfterVersion = false;
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
if (line.startsWith('# Command: /')) {
|
|
92
|
+
skipUntilAfterVersion = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (skipUntilAfterVersion && line.startsWith('**Version**:')) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (skipUntilAfterVersion && line.startsWith('**Description**:')) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (skipUntilAfterVersion && line.trim() === '---') {
|
|
102
|
+
skipUntilAfterVersion = false;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
filtered.push(line);
|
|
106
|
+
}
|
|
107
|
+
return filtered.join('\n').trimStart();
|
|
108
|
+
};
|
|
109
|
+
const adaptBodyForClaude = (markdown) => {
|
|
110
|
+
return markdown
|
|
111
|
+
.replace(/\/repofence\./g, '/repofence-')
|
|
112
|
+
.replace(/\/spec\/IDE_CURSOR\.md/g, '/spec/IDE_CLAUDE.md')
|
|
113
|
+
.replace(/Cursor chat/gi, 'Claude Code chat')
|
|
114
|
+
.replace(/Cursor AI/gi, 'Claude Code');
|
|
115
|
+
};
|
|
116
|
+
const toClaudeSkillMarkdown = (commandPath, markdown) => {
|
|
117
|
+
const skillName = toSkillName(commandPath);
|
|
118
|
+
const description = extractDescription(markdown);
|
|
119
|
+
const hasSideEffects = skillName.includes('build') ||
|
|
120
|
+
skillName.includes('update') ||
|
|
121
|
+
skillName.includes('finish') ||
|
|
122
|
+
skillName.includes('start') ||
|
|
123
|
+
skillName.includes('reverse');
|
|
124
|
+
const frontmatter = [
|
|
125
|
+
'---',
|
|
126
|
+
`name: ${skillName}`,
|
|
127
|
+
`description: ${description}`,
|
|
128
|
+
...(hasSideEffects ? ['disable-model-invocation: true'] : []),
|
|
129
|
+
'---',
|
|
130
|
+
'',
|
|
131
|
+
].join('\n');
|
|
132
|
+
const body = adaptBodyForClaude(stripCursorCommandHeader(markdown));
|
|
133
|
+
return { skillName, content: `${frontmatter}${body}\n` };
|
|
134
|
+
};
|
|
57
135
|
const installFromArchive = async (pack, options, baseDir) => {
|
|
136
|
+
const target = options.target || 'cursor';
|
|
58
137
|
const dir = commandsDir(baseDir);
|
|
59
138
|
await promises_1.default.mkdir(dir, { recursive: true });
|
|
60
139
|
const archiveUrl = pack.archiveUrl;
|
|
61
|
-
//
|
|
140
|
+
// Download manifest and recalculate hash (robustness)
|
|
62
141
|
let manifestHashCalculated = undefined;
|
|
63
142
|
if (pack.manifestUrl) {
|
|
64
143
|
const manifestBuf = await downloadBuffer(pack.manifestUrl);
|
|
65
144
|
manifestHashCalculated = sha256Base64(manifestBuf);
|
|
66
145
|
if (pack.manifestHash && pack.manifestHash !== manifestHashCalculated) {
|
|
67
|
-
throw new Error(`
|
|
146
|
+
throw new Error(`Invalid manifest hash. Expected ${pack.manifestHash}, got ${manifestHashCalculated}`);
|
|
68
147
|
}
|
|
69
148
|
}
|
|
70
149
|
const archiveBuf = await downloadBuffer(archiveUrl);
|
|
71
150
|
if (pack.archiveHash) {
|
|
72
151
|
const hash = sha256Base64(archiveBuf);
|
|
73
152
|
if (hash !== pack.archiveHash) {
|
|
74
|
-
throw new Error(`
|
|
153
|
+
throw new Error(`Invalid archive hash. Expected ${pack.archiveHash}, got ${hash}`);
|
|
75
154
|
}
|
|
76
155
|
}
|
|
77
|
-
//
|
|
156
|
+
// List files, then extract archive contents
|
|
78
157
|
const tmpDir = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'repofence-archive-'));
|
|
79
158
|
const archivePath = path_1.default.join(tmpDir, 'pack.tar.gz');
|
|
159
|
+
const extractedPath = path_1.default.join(tmpDir, 'extracted');
|
|
160
|
+
await promises_1.default.mkdir(extractedPath, { recursive: true });
|
|
80
161
|
await promises_1.default.writeFile(archivePath, archiveBuf);
|
|
81
|
-
|
|
82
|
-
await tar_1.default.t({
|
|
83
|
-
file: archivePath,
|
|
84
|
-
onentry: (entry) => {
|
|
85
|
-
if (entry.type === 'File') {
|
|
86
|
-
extractedFiles.push(entry.path);
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
// Validación de firma (manifestHash ya viene en el pack; archiveHash validada arriba)
|
|
162
|
+
// Signature validation (manifestHash from pack; archiveHash validated above)
|
|
91
163
|
await (0, exports.validateSignature)({
|
|
92
164
|
...pack,
|
|
93
165
|
manifestHash: pack.manifestHash || manifestHashCalculated,
|
|
94
166
|
});
|
|
95
167
|
await tar_1.default.x({
|
|
96
168
|
file: archivePath,
|
|
97
|
-
cwd:
|
|
169
|
+
cwd: extractedPath,
|
|
98
170
|
});
|
|
171
|
+
const manifest = target === 'claude'
|
|
172
|
+
? await installClaudeFromExtracted(extractedPath, pack, baseDir, options)
|
|
173
|
+
: await installCursorFromExtracted(extractedPath, pack, baseDir);
|
|
99
174
|
await promises_1.default.rm(tmpDir, { recursive: true, force: true });
|
|
175
|
+
if (options.verbose) {
|
|
176
|
+
console.log(`[repofence] installed from archive ${archiveUrl}`);
|
|
177
|
+
}
|
|
178
|
+
return manifest;
|
|
179
|
+
};
|
|
180
|
+
const installCursorFromExtracted = async (extractedPath, pack, baseDir) => {
|
|
181
|
+
const dir = commandsDir(baseDir);
|
|
182
|
+
await promises_1.default.mkdir(dir, { recursive: true });
|
|
183
|
+
const files = await listFilesRecursive(extractedPath);
|
|
184
|
+
for (const rel of files) {
|
|
185
|
+
const src = path_1.default.join(extractedPath, rel);
|
|
186
|
+
const dst = path_1.default.join(dir, rel);
|
|
187
|
+
await promises_1.default.mkdir(path_1.default.dirname(dst), { recursive: true });
|
|
188
|
+
await promises_1.default.copyFile(src, dst);
|
|
189
|
+
}
|
|
100
190
|
const manifest = {
|
|
101
191
|
pack_id: pack.packId,
|
|
102
192
|
pack_version: pack.packVersion,
|
|
103
193
|
installed_at: new Date().toISOString(),
|
|
104
194
|
expires_at: pack.expiresAt ?? null,
|
|
105
|
-
commands:
|
|
195
|
+
commands: files.map((f) => f.replace(/^\.\//, '')),
|
|
106
196
|
};
|
|
107
197
|
await (0, exports.writeManifest)(baseDir, manifest);
|
|
108
|
-
|
|
109
|
-
|
|
198
|
+
return manifest;
|
|
199
|
+
};
|
|
200
|
+
const installClaudeFromExtracted = async (extractedPath, pack, baseDir, options) => {
|
|
201
|
+
const dir = commandsDir(baseDir);
|
|
202
|
+
await promises_1.default.mkdir(dir, { recursive: true });
|
|
203
|
+
const files = await listFilesRecursive(extractedPath);
|
|
204
|
+
const markdownFiles = files.filter((f) => f.endsWith('.md'));
|
|
205
|
+
const artifacts = [];
|
|
206
|
+
for (const rel of markdownFiles) {
|
|
207
|
+
const sourcePath = path_1.default.join(extractedPath, rel);
|
|
208
|
+
const sourceMarkdown = await promises_1.default.readFile(sourcePath, 'utf8');
|
|
209
|
+
const { skillName, content } = toClaudeSkillMarkdown(rel, sourceMarkdown);
|
|
210
|
+
const targetPath = path_1.default.join(dir, skillName, 'SKILL.md');
|
|
211
|
+
if (!options.force && (await fileExists(targetPath))) {
|
|
212
|
+
throw new Error(`Skill already exists: ${targetPath} (use --force to overwrite)`);
|
|
213
|
+
}
|
|
214
|
+
await promises_1.default.mkdir(path_1.default.dirname(targetPath), { recursive: true });
|
|
215
|
+
await promises_1.default.writeFile(targetPath, content, 'utf8');
|
|
216
|
+
artifacts.push(path_1.default.relative(dir, targetPath));
|
|
110
217
|
}
|
|
218
|
+
const manifest = {
|
|
219
|
+
pack_id: pack.packId,
|
|
220
|
+
pack_version: pack.packVersion,
|
|
221
|
+
installed_at: new Date().toISOString(),
|
|
222
|
+
expires_at: pack.expiresAt ?? null,
|
|
223
|
+
commands: artifacts,
|
|
224
|
+
};
|
|
225
|
+
await (0, exports.writeManifest)(baseDir, manifest);
|
|
111
226
|
return manifest;
|
|
112
227
|
};
|
|
113
228
|
const installPack = async (pack, options = {}, baseDir) => {
|
|
114
|
-
|
|
229
|
+
const target = options.target || 'cursor';
|
|
230
|
+
// If archiveUrl is set, download tar.gz and extract.
|
|
115
231
|
if (pack.archiveUrl) {
|
|
116
232
|
return installFromArchive(pack, options, baseDir);
|
|
117
233
|
}
|
|
118
234
|
const dir = commandsDir(baseDir);
|
|
119
235
|
await promises_1.default.mkdir(dir, { recursive: true });
|
|
236
|
+
if (target === 'claude') {
|
|
237
|
+
const artifacts = [];
|
|
238
|
+
for (const file of pack.files) {
|
|
239
|
+
const source = Buffer.from(file.contentBase64, 'base64').toString('utf8');
|
|
240
|
+
const { skillName, content } = toClaudeSkillMarkdown(file.path, source);
|
|
241
|
+
const targetPath = path_1.default.join(dir, skillName, 'SKILL.md');
|
|
242
|
+
if (!options.force && (await fileExists(targetPath))) {
|
|
243
|
+
throw new Error(`Skill already exists: ${targetPath} (use --force to overwrite)`);
|
|
244
|
+
}
|
|
245
|
+
await promises_1.default.mkdir(path_1.default.dirname(targetPath), { recursive: true });
|
|
246
|
+
await promises_1.default.writeFile(targetPath, content, 'utf8');
|
|
247
|
+
artifacts.push(path_1.default.relative(dir, targetPath));
|
|
248
|
+
if (options.verbose) {
|
|
249
|
+
console.log(`[repofence] wrote skill ${targetPath}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const manifest = {
|
|
253
|
+
pack_id: pack.packId,
|
|
254
|
+
pack_version: pack.packVersion,
|
|
255
|
+
installed_at: new Date().toISOString(),
|
|
256
|
+
expires_at: pack.expiresAt ?? null,
|
|
257
|
+
commands: artifacts,
|
|
258
|
+
};
|
|
259
|
+
await (0, exports.writeManifest)(baseDir, manifest);
|
|
260
|
+
return manifest;
|
|
261
|
+
}
|
|
120
262
|
for (const file of pack.files) {
|
|
121
263
|
const target = path_1.default.join(dir, file.path);
|
|
122
264
|
if (!options.force && (await fileExists(target))) {
|
|
123
|
-
throw new Error(`
|
|
265
|
+
throw new Error(`File already exists: ${target} (use --force to overwrite)`);
|
|
124
266
|
}
|
|
125
267
|
await promises_1.default.mkdir(path_1.default.dirname(target), { recursive: true });
|
|
126
268
|
const buffer = Buffer.from(file.contentBase64, 'base64');
|
|
127
269
|
await promises_1.default.writeFile(target, buffer);
|
|
128
270
|
if (options.verbose) {
|
|
129
|
-
console.log(`[repofence]
|
|
271
|
+
console.log(`[repofence] wrote ${target}`);
|
|
130
272
|
}
|
|
131
273
|
}
|
|
132
274
|
const manifest = {
|
|
@@ -142,26 +284,13 @@ const installPack = async (pack, options = {}, baseDir) => {
|
|
|
142
284
|
exports.installPack = installPack;
|
|
143
285
|
const manifestFilePath = (baseDir) => manifestPath(baseDir);
|
|
144
286
|
exports.manifestFilePath = manifestFilePath;
|
|
145
|
-
/** Standard base64 or base64url (some APIs / env pastes). */
|
|
146
|
-
const decodeBase64Flexible = (input) => {
|
|
147
|
-
const trimmed = input.trim().replace(/\s/g, '');
|
|
148
|
-
if (!trimmed) {
|
|
149
|
-
throw new Error('Cadena vacía.');
|
|
150
|
-
}
|
|
151
|
-
const b64 = trimmed.includes('-') || trimmed.includes('_')
|
|
152
|
-
? trimmed.replace(/-/g, '+').replace(/_/g, '/')
|
|
153
|
-
: trimmed;
|
|
154
|
-
const pad = b64.length % 4;
|
|
155
|
-
const padded = pad ? b64 + '='.repeat(4 - pad) : b64;
|
|
156
|
-
return Buffer.from(padded, 'base64');
|
|
157
|
-
};
|
|
158
287
|
const validateSignature = async (_pack) => {
|
|
159
288
|
const pack = _pack;
|
|
160
|
-
//
|
|
289
|
+
// If there is no signature or hashes, skip validation (demo/mock mode).
|
|
161
290
|
if (!pack.signature || !pack.manifestHash) {
|
|
162
291
|
return true;
|
|
163
292
|
}
|
|
164
|
-
//
|
|
293
|
+
// Build payload same as backend: manifestHash[:archiveHash]
|
|
165
294
|
const payloadToVerify = pack.archiveHash
|
|
166
295
|
? `${pack.manifestHash}:${pack.archiveHash}`
|
|
167
296
|
: pack.manifestHash;
|
|
@@ -171,35 +300,14 @@ const validateSignature = async (_pack) => {
|
|
|
171
300
|
: Buffer.from(rawKey, 'base64').toString('utf8');
|
|
172
301
|
try {
|
|
173
302
|
const keyObject = crypto_1.default.createPublicKey(publicKeyPem);
|
|
174
|
-
const
|
|
175
|
-
const dataBuf = Buffer.from(payloadToVerify, 'utf8');
|
|
176
|
-
const keyType = keyObject.asymmetricKeyType;
|
|
177
|
-
let isValid;
|
|
178
|
-
if (keyType === 'ed25519') {
|
|
179
|
-
if (sigBuf.length !== 64) {
|
|
180
|
-
throw new Error(`Firma Ed25519: tras decodificar se esperaban 64 bytes, hay ${sigBuf.length}. ` +
|
|
181
|
-
'Suele indicar que el API firmó con otro algoritmo (p. ej. RSA) o la firma está corrupta. ' +
|
|
182
|
-
'El servidor debe usar el par Ed25519 que corresponde a la clave pública del CLI.');
|
|
183
|
-
}
|
|
184
|
-
isValid = crypto_1.default.verify(null, dataBuf, keyObject, sigBuf);
|
|
185
|
-
}
|
|
186
|
-
else if (keyType === 'rsa') {
|
|
187
|
-
isValid = crypto_1.default.verify('sha256', dataBuf, keyObject, sigBuf);
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
throw new Error(`Tipo de clave pública no soportado: ${String(keyType)}`);
|
|
191
|
-
}
|
|
303
|
+
const isValid = crypto_1.default.verify(null, Buffer.from(payloadToVerify), keyObject, Buffer.from(pack.signature, 'base64'));
|
|
192
304
|
if (!isValid) {
|
|
193
|
-
throw new Error('
|
|
305
|
+
throw new Error('Invalid pack signature.');
|
|
194
306
|
}
|
|
195
307
|
return true;
|
|
196
308
|
}
|
|
197
309
|
catch (error) {
|
|
198
|
-
const msg = error instanceof Error ? error.message : '
|
|
199
|
-
if (msg.includes('asn1') || msg.includes('DECODER') || msg.includes('wrong tag')) {
|
|
200
|
-
throw new Error(`${msg}. Comprueba que REPOFENCE_SIGNING_KEY en el API sea el par Ed25519 de public_key.pem ` +
|
|
201
|
-
'(o que REPOFENCE_PUBLIC_KEY en el cliente sea la clave pública que corresponde a la privada del servidor).');
|
|
202
|
-
}
|
|
310
|
+
const msg = error instanceof Error ? error.message : 'signature_validation_error';
|
|
203
311
|
throw new Error(msg);
|
|
204
312
|
}
|
|
205
313
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getPackIdForTarget = exports.isIdeTarget = exports.getTargetProfile = void 0;
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const home = os_1.default.homedir();
|
|
10
|
+
const TARGETS = {
|
|
11
|
+
cursor: {
|
|
12
|
+
id: 'cursor',
|
|
13
|
+
displayName: 'Cursor',
|
|
14
|
+
installBaseDir: path_1.default.join(home, '.cursor', 'commands'),
|
|
15
|
+
helpCommand: '/repofence.help',
|
|
16
|
+
packIdEnv: 'REPOFENCE_PACK_ID',
|
|
17
|
+
specGuideTemplate: 'IDE_CURSOR.md',
|
|
18
|
+
},
|
|
19
|
+
claude: {
|
|
20
|
+
id: 'claude',
|
|
21
|
+
displayName: 'Claude Code',
|
|
22
|
+
installBaseDir: path_1.default.join(home, '.claude', 'skills'),
|
|
23
|
+
helpCommand: '/repofence-reverse',
|
|
24
|
+
packIdEnv: 'REPOFENCE_CLAUDE_PACK_ID',
|
|
25
|
+
specGuideTemplate: 'IDE_CLAUDE.md',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const getTargetProfile = (target) => TARGETS[target];
|
|
29
|
+
exports.getTargetProfile = getTargetProfile;
|
|
30
|
+
const isIdeTarget = (value) => value === 'cursor' || value === 'claude';
|
|
31
|
+
exports.isIdeTarget = isIdeTarget;
|
|
32
|
+
const getPackIdForTarget = (target) => {
|
|
33
|
+
const profile = (0, exports.getTargetProfile)(target);
|
|
34
|
+
if (profile.id === 'claude') {
|
|
35
|
+
return process.env[profile.packIdEnv] || process.env.REPOFENCE_PACK_ID || 'core';
|
|
36
|
+
}
|
|
37
|
+
return process.env[profile.packIdEnv] || 'core';
|
|
38
|
+
};
|
|
39
|
+
exports.getPackIdForTarget = getPackIdForTarget;
|
|
40
|
+
//# sourceMappingURL=targets.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Repofence SDD Instructions
|
|
2
|
+
|
|
3
|
+
Use the spec layer as the primary source of truth in this repository.
|
|
4
|
+
|
|
5
|
+
## Mandatory workflow
|
|
6
|
+
|
|
7
|
+
1. Read `/spec/AI_RULES.md` first.
|
|
8
|
+
2. Read `/spec/SYSTEM_OVERVIEW.md`, `/spec/CORE_FLOWS.md`, and `/spec/BOUNDARIES.md`.
|
|
9
|
+
3. For brownfield projects, run `/repofence-reverse` before planning implementation work.
|
|
10
|
+
4. If specs are missing or stale, say so explicitly and propose how to refresh.
|
|
11
|
+
|
|
12
|
+
## Commanding model
|
|
13
|
+
|
|
14
|
+
- Use Repofence skills for repeatable workflows:
|
|
15
|
+
- `/repofence-reverse`
|
|
16
|
+
- `/repofence-ask`
|
|
17
|
+
- `/repofence-plan`
|
|
18
|
+
- `/repofence-build`
|
|
19
|
+
- `/repofence-review`
|
|
20
|
+
- `/repofence-finish`
|
|
21
|
+
|
|
22
|
+
## Boundaries
|
|
23
|
+
|
|
24
|
+
- Do not invent architecture details not present in specs.
|
|
25
|
+
- Do not silently violate constraints in `BOUNDARIES.md`.
|
|
26
|
+
- If implementation work conflicts with specs, stop and ask for direction.
|
|
27
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# SDD Rules (Repofence)
|
|
2
|
+
|
|
3
|
+
These rules apply when editing this project with Claude Code.
|
|
4
|
+
|
|
5
|
+
## Spec-first
|
|
6
|
+
|
|
7
|
+
- Always read `spec/AI_RULES.md` before implementation.
|
|
8
|
+
- Treat `spec/SYSTEM_OVERVIEW.md`, `spec/CORE_FLOWS.md`, and `spec/BOUNDARIES.md` as authoritative context.
|
|
9
|
+
|
|
10
|
+
## Brownfield priority
|
|
11
|
+
|
|
12
|
+
- Use `/repofence-reverse` early to keep specs aligned with code.
|
|
13
|
+
- If spec coverage is incomplete, label unknowns explicitly instead of guessing.
|
|
14
|
+
|
|
15
|
+
## Quality checks
|
|
16
|
+
|
|
17
|
+
- Validate changes against boundaries before finalizing.
|
|
18
|
+
- Keep specs updated when workflows, boundaries, or invariants change.
|
|
19
|
+
- Prefer small, traceable diffs from task to implementation.
|
|
20
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Claude Code Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide provides ready-to-copy prompts for using Claude Code with `{{PROJECT_NAME}}`.
|
|
4
|
+
|
|
5
|
+
## First Prompt
|
|
6
|
+
|
|
7
|
+
Use this at the start of every session:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Read /spec/AI_RULES.md first, then /spec/SYSTEM_OVERVIEW.md, /spec/CORE_FLOWS.md, and /spec/BOUNDARIES.md. Summarize constraints before changing code.
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Brownfield Fast Path (reverse-first)
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Run /repofence-reverse first to refresh the spec layer for this codebase. Then use /repofence-ask to answer questions only from specs.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Recommended Skill Flow
|
|
20
|
+
|
|
21
|
+
1. `/repofence-reverse` for brownfield discovery
|
|
22
|
+
2. `/repofence-ask` for spec-only Q&A
|
|
23
|
+
3. `/repofence-plan` before code changes
|
|
24
|
+
4. `/repofence-build` for task execution
|
|
25
|
+
5. `/repofence-review` and `/repofence-finish` to close work
|
|
26
|
+
|
|
27
|
+
## Guardrails
|
|
28
|
+
|
|
29
|
+
- Use specs as source of truth.
|
|
30
|
+
- If a spec is stale or missing, call it out explicitly.
|
|
31
|
+
- Never guess implementation details when specs do not contain them.
|
|
32
|
+
- Update specs when behavior or boundaries change.
|
|
33
|
+
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repofence",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Repofence CLI (packs + backend auth)",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"repofence": "
|
|
7
|
+
"repofence": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/**/*.js",
|
|
@@ -20,9 +20,7 @@
|
|
|
20
20
|
"start:cli": "node dist/cli.js",
|
|
21
21
|
"dev": "ts-node src/cli.ts",
|
|
22
22
|
"backend": "ts-node backend/server.ts",
|
|
23
|
-
"backend:start": "node backend-dist/server.js"
|
|
24
|
-
"inspect:signing": "ts-node scripts/inspect-signing-key.ts",
|
|
25
|
-
"verify:pack-sig": "ts-node scripts/verify-pack-signature-files.ts"
|
|
23
|
+
"backend:start": "node backend-dist/server.js"
|
|
26
24
|
},
|
|
27
25
|
"keywords": [
|
|
28
26
|
"sdd",
|
|
@@ -39,7 +37,6 @@
|
|
|
39
37
|
"commander": "^11.1.0",
|
|
40
38
|
"dotenv": "^16.4.5",
|
|
41
39
|
"open": "^11.0.0",
|
|
42
|
-
"repofence": "^0.1.2",
|
|
43
40
|
"tar": "^6.2.1"
|
|
44
41
|
},
|
|
45
42
|
"devDependencies": {
|