@rafter-security/cli 0.1.0 → 0.4.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.
@@ -0,0 +1,346 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { createHash } from "crypto";
5
+ import { ConfigManager } from "../core/config-manager.js";
6
+ import { fileURLToPath } from 'url';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ export class SkillManager {
10
+ constructor() {
11
+ this.configManager = new ConfigManager();
12
+ }
13
+ /**
14
+ * Get path to OpenClaw skills directory
15
+ */
16
+ getOpenClawSkillsDir() {
17
+ return path.join(os.homedir(), ".openclaw", "skills");
18
+ }
19
+ /**
20
+ * Get path to Rafter Security skill in OpenClaw
21
+ */
22
+ getRafterSkillPath() {
23
+ return path.join(this.getOpenClawSkillsDir(), "rafter-security.md");
24
+ }
25
+ /**
26
+ * Get path to old skill-auditor (for migration)
27
+ */
28
+ getOldSkillAuditorPath() {
29
+ return path.join(this.getOpenClawSkillsDir(), "rafter-skill-auditor.md");
30
+ }
31
+ /**
32
+ * Get path to Rafter Security skill source in CLI resources
33
+ */
34
+ getRafterSkillSourcePath() {
35
+ // Go up from src/utils/ to node/ then into resources/
36
+ return path.join(__dirname, "..", "..", "resources", "rafter-security-skill.md");
37
+ }
38
+ /**
39
+ * Get path to backups directory
40
+ */
41
+ getBackupsDir() {
42
+ return path.join(this.getOpenClawSkillsDir(), ".backups");
43
+ }
44
+ /**
45
+ * Check if OpenClaw is installed (skills directory exists)
46
+ */
47
+ isOpenClawInstalled() {
48
+ return fs.existsSync(this.getOpenClawSkillsDir());
49
+ }
50
+ /**
51
+ * Check if Rafter Security skill is installed
52
+ */
53
+ isRafterSkillInstalled() {
54
+ return fs.existsSync(this.getRafterSkillPath());
55
+ }
56
+ /**
57
+ * Check if old skill-auditor is installed (for migration)
58
+ */
59
+ hasOldSkillAuditor() {
60
+ return fs.existsSync(this.getOldSkillAuditorPath());
61
+ }
62
+ /**
63
+ * Parse frontmatter metadata from skill file
64
+ */
65
+ parseSkillMetadata(content) {
66
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
67
+ if (!frontmatterMatch) {
68
+ return null;
69
+ }
70
+ const frontmatter = frontmatterMatch[1];
71
+ const metadata = {};
72
+ frontmatter.split('\n').forEach(line => {
73
+ const [key, ...valueParts] = line.split(':');
74
+ if (key && valueParts.length > 0) {
75
+ metadata[key.trim()] = valueParts.join(':').trim();
76
+ }
77
+ });
78
+ if (!metadata.name || !metadata.version) {
79
+ return null;
80
+ }
81
+ return metadata;
82
+ }
83
+ /**
84
+ * Get version of installed Rafter Security skill
85
+ */
86
+ getInstalledVersion() {
87
+ if (!this.isRafterSkillInstalled()) {
88
+ return null;
89
+ }
90
+ try {
91
+ const content = fs.readFileSync(this.getRafterSkillPath(), "utf-8");
92
+ const metadata = this.parseSkillMetadata(content);
93
+ return metadata?.version || null;
94
+ }
95
+ catch (e) {
96
+ return null;
97
+ }
98
+ }
99
+ /**
100
+ * Get version of Rafter Security skill in CLI resources
101
+ */
102
+ getSourceVersion() {
103
+ try {
104
+ const content = fs.readFileSync(this.getRafterSkillSourcePath(), "utf-8");
105
+ const metadata = this.parseSkillMetadata(content);
106
+ return metadata?.version || null;
107
+ }
108
+ catch (e) {
109
+ return null;
110
+ }
111
+ }
112
+ /**
113
+ * Calculate hash of file content (excluding frontmatter for version)
114
+ */
115
+ calculateContentHash(content) {
116
+ // Remove frontmatter for hash calculation
117
+ const withoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n/, '');
118
+ return createHash('sha256').update(withoutFrontmatter).digest('hex').substring(0, 8);
119
+ }
120
+ /**
121
+ * Check if installed skill has been modified by user
122
+ */
123
+ isSkillModified() {
124
+ if (!this.isRafterSkillInstalled()) {
125
+ return false;
126
+ }
127
+ try {
128
+ const installedContent = fs.readFileSync(this.getRafterSkillPath(), "utf-8");
129
+ const sourceContent = fs.readFileSync(this.getRafterSkillSourcePath(), "utf-8");
130
+ const installedHash = this.calculateContentHash(installedContent);
131
+ const sourceHash = this.calculateContentHash(sourceContent);
132
+ return installedHash !== sourceHash;
133
+ }
134
+ catch (e) {
135
+ return false;
136
+ }
137
+ }
138
+ /**
139
+ * Migrate from old separate skill-auditor to combined Rafter Security skill
140
+ */
141
+ async migrateOldSkill() {
142
+ if (this.hasOldSkillAuditor()) {
143
+ try {
144
+ // Remove old skill-auditor file
145
+ fs.unlinkSync(this.getOldSkillAuditorPath());
146
+ console.log("✓ Migrated from separate skill-auditor to combined Rafter Security skill");
147
+ }
148
+ catch (e) {
149
+ // Ignore migration errors
150
+ }
151
+ }
152
+ }
153
+ /**
154
+ * Install Rafter Security skill to OpenClaw
155
+ */
156
+ async installRafterSkill(force = false) {
157
+ if (!this.isOpenClawInstalled()) {
158
+ return false;
159
+ }
160
+ const skillPath = this.getRafterSkillPath();
161
+ const sourcePath = this.getRafterSkillSourcePath();
162
+ // Check if already installed and not forcing
163
+ if (!force && this.isRafterSkillInstalled()) {
164
+ return true;
165
+ }
166
+ try {
167
+ // Ensure skills directory exists
168
+ const skillsDir = this.getOpenClawSkillsDir();
169
+ if (!fs.existsSync(skillsDir)) {
170
+ fs.mkdirSync(skillsDir, { recursive: true });
171
+ }
172
+ // Copy skill file
173
+ const sourceContent = fs.readFileSync(sourcePath, "utf-8");
174
+ fs.writeFileSync(skillPath, sourceContent, "utf-8");
175
+ // Update config
176
+ const version = this.getSourceVersion();
177
+ if (version) {
178
+ this.configManager.set("agent.skills.installedVersion", version);
179
+ this.configManager.set("agent.skills.lastChecked", new Date().toISOString());
180
+ }
181
+ // Migrate old skill-auditor if present
182
+ await this.migrateOldSkill();
183
+ return true;
184
+ }
185
+ catch (e) {
186
+ console.error(`Failed to install Rafter Security skill: ${e}`);
187
+ return false;
188
+ }
189
+ }
190
+ /**
191
+ * Backup current skill before updating
192
+ */
193
+ backupSkill() {
194
+ if (!this.isRafterSkillInstalled()) {
195
+ return false;
196
+ }
197
+ try {
198
+ const backupsDir = this.getBackupsDir();
199
+ if (!fs.existsSync(backupsDir)) {
200
+ fs.mkdirSync(backupsDir, { recursive: true });
201
+ }
202
+ const version = this.getInstalledVersion() || "unknown";
203
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
204
+ const backupPath = path.join(backupsDir, `rafter-security.md.v${version}.${timestamp}`);
205
+ const content = fs.readFileSync(this.getRafterSkillPath(), "utf-8");
206
+ fs.writeFileSync(backupPath, content, "utf-8");
207
+ // Keep only last 3 backups
208
+ this.cleanupOldBackups(3);
209
+ return true;
210
+ }
211
+ catch (e) {
212
+ console.error(`Failed to backup skill: ${e}`);
213
+ return false;
214
+ }
215
+ }
216
+ /**
217
+ * Remove old backups, keeping only the most recent N
218
+ */
219
+ cleanupOldBackups(keep) {
220
+ const backupsDir = this.getBackupsDir();
221
+ if (!fs.existsSync(backupsDir)) {
222
+ return;
223
+ }
224
+ try {
225
+ const files = fs.readdirSync(backupsDir)
226
+ .filter(f => f.startsWith("rafter-security.md.v") || f.startsWith("rafter-skill-auditor.md.v"))
227
+ .map(f => ({
228
+ name: f,
229
+ path: path.join(backupsDir, f),
230
+ mtime: fs.statSync(path.join(backupsDir, f)).mtime
231
+ }))
232
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
233
+ // Remove old backups
234
+ files.slice(keep).forEach(file => {
235
+ fs.unlinkSync(file.path);
236
+ });
237
+ }
238
+ catch (e) {
239
+ // Ignore cleanup errors
240
+ }
241
+ }
242
+ /**
243
+ * Update Rafter Security skill to latest version
244
+ */
245
+ async updateRafterSkill(options = {}) {
246
+ if (!this.isOpenClawInstalled()) {
247
+ return { updated: false, message: "OpenClaw not installed" };
248
+ }
249
+ if (!this.isRafterSkillInstalled()) {
250
+ // Install if not present
251
+ const installed = await this.installRafterSkill();
252
+ return {
253
+ updated: installed,
254
+ message: installed ? "Rafter Security skill installed" : "Failed to install Rafter Security skill"
255
+ };
256
+ }
257
+ const installedVersion = this.getInstalledVersion();
258
+ const sourceVersion = this.getSourceVersion();
259
+ if (!sourceVersion) {
260
+ return { updated: false, message: "Could not determine source version" };
261
+ }
262
+ // Check if update needed
263
+ if (!options.force && installedVersion === sourceVersion) {
264
+ return { updated: false, message: "Rafter Security skill is up to date" };
265
+ }
266
+ // Check if modified
267
+ if (!options.force && this.isSkillModified()) {
268
+ return {
269
+ updated: false,
270
+ message: "Skill has been modified. Use --force to overwrite."
271
+ };
272
+ }
273
+ // Backup if requested
274
+ const config = this.configManager.load();
275
+ if (options.backup !== false && config.agent?.skills.backupBeforeUpdate) {
276
+ this.backupSkill();
277
+ }
278
+ // Install new version
279
+ const installed = await this.installRafterSkill(true);
280
+ if (installed) {
281
+ return {
282
+ updated: true,
283
+ message: `Updated Rafter Security skill: ${installedVersion || 'unknown'} → ${sourceVersion}`
284
+ };
285
+ }
286
+ else {
287
+ return {
288
+ updated: false,
289
+ message: "Failed to update Rafter Security skill"
290
+ };
291
+ }
292
+ }
293
+ /**
294
+ * Remove Rafter Security skill
295
+ */
296
+ removeRafterSkill() {
297
+ if (!this.isRafterSkillInstalled()) {
298
+ return false;
299
+ }
300
+ try {
301
+ fs.unlinkSync(this.getRafterSkillPath());
302
+ // Clear config
303
+ this.configManager.set("agent.skills.installedVersion", undefined);
304
+ return true;
305
+ }
306
+ catch (e) {
307
+ console.error(`Failed to remove Rafter Security skill: ${e}`);
308
+ return false;
309
+ }
310
+ }
311
+ /**
312
+ * Check if skill update is available
313
+ */
314
+ isUpdateAvailable() {
315
+ if (!this.isRafterSkillInstalled()) {
316
+ return false;
317
+ }
318
+ const installedVersion = this.getInstalledVersion();
319
+ const sourceVersion = this.getSourceVersion();
320
+ if (!installedVersion || !sourceVersion) {
321
+ return false;
322
+ }
323
+ return installedVersion !== sourceVersion;
324
+ }
325
+ /**
326
+ * Check for updates and auto-update if configured
327
+ */
328
+ async checkAndUpdate(silent = true) {
329
+ const config = this.configManager.load();
330
+ // Skip if auto-update disabled
331
+ if (!config.agent?.skills.autoUpdate) {
332
+ return;
333
+ }
334
+ // Check if update available
335
+ if (!this.isUpdateAvailable()) {
336
+ return;
337
+ }
338
+ // Update silently
339
+ const result = await this.updateRafterSkill({ backup: true });
340
+ if (!silent && result.updated) {
341
+ console.log(`✓ ${result.message}`);
342
+ }
343
+ // Update last checked timestamp
344
+ this.configManager.set("agent.skills.lastChecked", new Date().toISOString());
345
+ }
346
+ }
@@ -0,0 +1,93 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import axios from "axios";
5
+ const UPDATE_CHECK_FILE = path.join(os.homedir(), ".rafter", "update-check.json");
6
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org/@rafter-security/cli/latest";
7
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
8
+ function readCache() {
9
+ try {
10
+ if (!fs.existsSync(UPDATE_CHECK_FILE))
11
+ return null;
12
+ return JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, "utf-8"));
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function writeCache(cache) {
19
+ try {
20
+ const dir = path.dirname(UPDATE_CHECK_FILE);
21
+ if (!fs.existsSync(dir))
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify(cache), "utf-8");
24
+ }
25
+ catch {
26
+ // Silent fail — update check is best-effort
27
+ }
28
+ }
29
+ function shouldCheck(cache) {
30
+ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION)
31
+ return false;
32
+ if (!cache)
33
+ return true;
34
+ const elapsed = Date.now() - new Date(cache.lastChecked).getTime();
35
+ return elapsed > CHECK_INTERVAL_MS;
36
+ }
37
+ function isNewer(current, latest) {
38
+ const c = current.split(".").map(Number);
39
+ const l = latest.split(".").map(Number);
40
+ for (let i = 0; i < 3; i++) {
41
+ if ((l[i] || 0) > (c[i] || 0))
42
+ return true;
43
+ if ((l[i] || 0) < (c[i] || 0))
44
+ return false;
45
+ }
46
+ return false;
47
+ }
48
+ /**
49
+ * Check npm registry for latest version. Non-blocking, cached, silent on failure.
50
+ *
51
+ * Behavior:
52
+ * - Hits the registry at most once per 24h
53
+ * - Shows the update notice exactly once per new version discovered
54
+ * - After the user sees the notice, stays silent until a newer version appears
55
+ * - Skips entirely in CI environments
56
+ */
57
+ export async function checkForUpdate(currentVersion) {
58
+ const cache = readCache();
59
+ if (!shouldCheck(cache)) {
60
+ // Within 24h window — no registry hit, no notice
61
+ return null;
62
+ }
63
+ try {
64
+ const res = await axios.get(NPM_REGISTRY_URL, { timeout: 3000 });
65
+ const latestVersion = res.data.version;
66
+ const updateAvailable = isNewer(currentVersion, latestVersion);
67
+ const alreadyNotified = cache?.notifiedVersion === latestVersion;
68
+ if (updateAvailable && !alreadyNotified) {
69
+ // New version we haven't told the user about yet
70
+ writeCache({
71
+ lastChecked: new Date().toISOString(),
72
+ latestVersion,
73
+ currentVersion,
74
+ notifiedVersion: latestVersion,
75
+ });
76
+ return formatNotice(currentVersion, latestVersion);
77
+ }
78
+ // Either up-to-date or already notified for this version
79
+ writeCache({
80
+ lastChecked: new Date().toISOString(),
81
+ latestVersion,
82
+ currentVersion,
83
+ notifiedVersion: cache?.notifiedVersion,
84
+ });
85
+ }
86
+ catch {
87
+ // Silent fail — network issues, registry down, etc.
88
+ }
89
+ return null;
90
+ }
91
+ function formatNotice(current, latest) {
92
+ return `\n Update available: ${current} → ${latest}\n Run: npm install -g @rafter-security/cli@latest\n Or: pip install --upgrade rafter-cli\n`;
93
+ }
package/package.json CHANGED
@@ -1,29 +1,34 @@
1
1
  {
2
2
  "name": "@rafter-security/cli",
3
- "version": "0.1.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "rafter": "./dist/index.js"
7
7
  },
8
- "files": ["dist"],
8
+ "files": [
9
+ "dist"
10
+ ],
9
11
  "scripts": {
10
12
  "build": "tsc -p tsconfig.json",
11
13
  "prepublishOnly": "pnpm run build",
12
14
  "test": "vitest"
13
15
  },
14
- "engines": { "node": ">=18" },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
15
19
  "license": "MIT",
16
20
  "dependencies": {
17
- "commander": "^11.1.0",
18
21
  "axios": "^1.6.8",
19
- "dotenv": "^16.4.5",
20
22
  "chalk": "^5.3.0",
21
- "ora": "^7.0.1"
23
+ "commander": "^11.1.0",
24
+ "dotenv": "^16.4.5",
25
+ "ora": "^7.0.1",
26
+ "tar": "^7.5.7"
22
27
  },
23
28
  "devDependencies": {
29
+ "@types/node": "^20.11.30",
24
30
  "tsx": "^4.7.0",
25
31
  "typescript": "^5.4.5",
26
- "@types/node": "^20.11.30",
27
32
  "vitest": "^1.5.0"
28
33
  }
29
- }
34
+ }