simba-skills 0.1.2
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 +181 -0
- package/package.json +59 -0
- package/src/commands/.gitkeep +0 -0
- package/src/commands/adopt.ts +232 -0
- package/src/commands/assign.ts +110 -0
- package/src/commands/backup.ts +121 -0
- package/src/commands/detect.ts +63 -0
- package/src/commands/doctor.ts +169 -0
- package/src/commands/import.ts +99 -0
- package/src/commands/install.ts +486 -0
- package/src/commands/list.ts +64 -0
- package/src/commands/manage.ts +12 -0
- package/src/commands/migrate.ts +137 -0
- package/src/commands/restore.ts +175 -0
- package/src/commands/snapshots.ts +39 -0
- package/src/commands/status.ts +70 -0
- package/src/commands/sync.ts +108 -0
- package/src/commands/unassign.ts +68 -0
- package/src/commands/undo.ts +57 -0
- package/src/commands/uninstall.ts +134 -0
- package/src/commands/update.ts +251 -0
- package/src/core/agent-registry.ts +105 -0
- package/src/core/config-store.ts +89 -0
- package/src/core/registry-store.ts +28 -0
- package/src/core/skill-manager.ts +99 -0
- package/src/core/skills-store.ts +79 -0
- package/src/core/snapshot.ts +123 -0
- package/src/core/types.ts +75 -0
- package/src/index.ts +33 -0
- package/src/tui/.gitkeep +0 -0
- package/src/tui/matrix.ts +189 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/diff.ts +53 -0
- package/src/utils/hash.ts +47 -0
- package/src/utils/paths.ts +30 -0
- package/src/utils/prompts.ts +85 -0
- package/src/utils/symlinks.ts +34 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { ConfigStore } from "../core/config-store"
|
|
3
|
+
import { AgentRegistry } from "../core/agent-registry"
|
|
4
|
+
import { SnapshotManager } from "../core/snapshot"
|
|
5
|
+
import { getConfigPath, getSnapshotsDir, expandPath } from "../utils/paths"
|
|
6
|
+
import { selectFromList, selectAgent, inputText } from "../utils/prompts"
|
|
7
|
+
import { mkdir, readFile } from "node:fs/promises"
|
|
8
|
+
import { join, dirname } from "node:path"
|
|
9
|
+
import * as tar from "tar"
|
|
10
|
+
|
|
11
|
+
export default defineCommand({
|
|
12
|
+
meta: {
|
|
13
|
+
name: "restore",
|
|
14
|
+
description: "Restore skills from backup",
|
|
15
|
+
},
|
|
16
|
+
args: {
|
|
17
|
+
path: {
|
|
18
|
+
type: "positional",
|
|
19
|
+
description: "Backup path (.tar.gz)",
|
|
20
|
+
},
|
|
21
|
+
to: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Restore to specific agent only",
|
|
24
|
+
},
|
|
25
|
+
snapshot: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Restore from snapshot ID instead of backup file",
|
|
28
|
+
},
|
|
29
|
+
dryRun: {
|
|
30
|
+
type: "boolean",
|
|
31
|
+
alias: "n",
|
|
32
|
+
description: "Preview changes without applying",
|
|
33
|
+
default: false,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
async run({ args }) {
|
|
37
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
38
|
+
const config = await configStore.load()
|
|
39
|
+
|
|
40
|
+
let snapshotId = args.snapshot
|
|
41
|
+
let backupPath = args.path
|
|
42
|
+
|
|
43
|
+
// Interactive mode if no path or snapshot provided
|
|
44
|
+
if (!backupPath && !snapshotId) {
|
|
45
|
+
const mode = await selectFromList("Restore from:", [
|
|
46
|
+
{ value: "snapshot", label: "Snapshot", hint: "restore from auto-saved snapshot" },
|
|
47
|
+
{ value: "backup", label: "Backup file", hint: "restore from .tar.gz archive" },
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
if (mode === "snapshot") {
|
|
51
|
+
const snapshots = new SnapshotManager(getSnapshotsDir(), config.snapshots.maxCount)
|
|
52
|
+
const list = await snapshots.listSnapshots()
|
|
53
|
+
|
|
54
|
+
if (list.length === 0) {
|
|
55
|
+
console.log("No snapshots available.")
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
snapshotId = await selectFromList(
|
|
60
|
+
"Select snapshot:",
|
|
61
|
+
list.map((s) => ({
|
|
62
|
+
value: s.id,
|
|
63
|
+
label: s.id,
|
|
64
|
+
hint: `${s.reason} (${s.skills.length} skills)`,
|
|
65
|
+
}))
|
|
66
|
+
)
|
|
67
|
+
} else {
|
|
68
|
+
backupPath = await inputText("Backup file path:", { placeholder: "./backup.tar.gz" })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle snapshot restore
|
|
73
|
+
if (snapshotId) {
|
|
74
|
+
const snapshots = new SnapshotManager(
|
|
75
|
+
getSnapshotsDir(),
|
|
76
|
+
config.snapshots.maxCount
|
|
77
|
+
)
|
|
78
|
+
const list = await snapshots.listSnapshots()
|
|
79
|
+
const snapshot = list.find((s) => s.id === snapshotId)
|
|
80
|
+
|
|
81
|
+
if (!snapshot) {
|
|
82
|
+
console.error(`Snapshot not found: ${snapshotId}`)
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(`\nRestoring from snapshot: ${snapshot.id}`)
|
|
87
|
+
console.log(`Skills: ${snapshot.skills.join(", ")}`)
|
|
88
|
+
|
|
89
|
+
if (args.dryRun) {
|
|
90
|
+
console.log("\n(dry run - no changes made)")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Restore to all detected agents or specific one
|
|
95
|
+
const targetAgents = args.to
|
|
96
|
+
? [args.to]
|
|
97
|
+
: Object.entries(config.agents)
|
|
98
|
+
.filter(([_, a]) => a.detected)
|
|
99
|
+
.map(([id]) => id)
|
|
100
|
+
|
|
101
|
+
for (const agentId of targetAgents) {
|
|
102
|
+
const agent = config.agents[agentId]
|
|
103
|
+
if (!agent) continue
|
|
104
|
+
await snapshots.restore(snapshotId, expandPath(agent.globalPath))
|
|
105
|
+
console.log(`Restored to ${agent.name}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log("\nRestore complete!")
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle backup file restore
|
|
113
|
+
if (!backupPath) {
|
|
114
|
+
console.error("No backup path provided.")
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const tempDir = join(dirname(backupPath), `.simba-restore-${Date.now()}`)
|
|
119
|
+
await mkdir(tempDir, { recursive: true })
|
|
120
|
+
|
|
121
|
+
// Extract backup
|
|
122
|
+
await tar.extract({
|
|
123
|
+
file: backupPath,
|
|
124
|
+
cwd: tempDir,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Read manifest
|
|
128
|
+
const manifest = JSON.parse(
|
|
129
|
+
await readFile(join(tempDir, "manifest.json"), "utf-8")
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
console.log(`\nRestoring from backup: ${backupPath}`)
|
|
133
|
+
console.log(`Created: ${manifest.created}`)
|
|
134
|
+
console.log(`Skills: ${Object.keys(manifest.skills).length}`)
|
|
135
|
+
|
|
136
|
+
if (args.dryRun) {
|
|
137
|
+
console.log("\nWould restore:")
|
|
138
|
+
for (const skillName of Object.keys(manifest.skills)) {
|
|
139
|
+
console.log(` ${skillName}`)
|
|
140
|
+
}
|
|
141
|
+
console.log("\n(dry run - no changes made)")
|
|
142
|
+
await Bun.$`rm -rf ${tempDir}`
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Determine target agents
|
|
147
|
+
const targetAgents = args.to
|
|
148
|
+
? [args.to]
|
|
149
|
+
: Object.entries(config.agents)
|
|
150
|
+
.filter(([_, a]) => a.detected)
|
|
151
|
+
.map(([id]) => id)
|
|
152
|
+
|
|
153
|
+
// Copy skills to targets
|
|
154
|
+
for (const agentId of targetAgents) {
|
|
155
|
+
const agent = config.agents[agentId]
|
|
156
|
+
if (!agent) continue
|
|
157
|
+
|
|
158
|
+
const skillsPath = expandPath(agent.globalPath)
|
|
159
|
+
await mkdir(skillsPath, { recursive: true })
|
|
160
|
+
|
|
161
|
+
for (const skillName of Object.keys(manifest.skills)) {
|
|
162
|
+
const sourcePath = join(tempDir, "skills", skillName)
|
|
163
|
+
const destPath = join(skillsPath, skillName)
|
|
164
|
+
await Bun.$`cp -r ${sourcePath} ${destPath}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`Restored to ${agent.name}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Cleanup temp
|
|
171
|
+
await Bun.$`rm -rf ${tempDir}`
|
|
172
|
+
|
|
173
|
+
console.log("\nRestore complete!")
|
|
174
|
+
},
|
|
175
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { ConfigStore } from "../core/config-store"
|
|
3
|
+
import { SnapshotManager } from "../core/snapshot"
|
|
4
|
+
import { getConfigPath, getSnapshotsDir } from "../utils/paths"
|
|
5
|
+
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "snapshots",
|
|
9
|
+
description: "List available snapshots",
|
|
10
|
+
},
|
|
11
|
+
async run() {
|
|
12
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
13
|
+
const config = await configStore.load()
|
|
14
|
+
|
|
15
|
+
const snapshots = new SnapshotManager(
|
|
16
|
+
getSnapshotsDir(),
|
|
17
|
+
config.snapshots.maxCount
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const list = await snapshots.listSnapshots()
|
|
21
|
+
|
|
22
|
+
if (list.length === 0) {
|
|
23
|
+
console.log("No snapshots available.")
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log("\nAvailable snapshots:\n")
|
|
28
|
+
|
|
29
|
+
for (const snapshot of list) {
|
|
30
|
+
console.log(` ${snapshot.id}`)
|
|
31
|
+
console.log(` Reason: ${snapshot.reason}`)
|
|
32
|
+
console.log(` Skills: ${snapshot.skills.length}`)
|
|
33
|
+
console.log("")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`Total: ${list.length} snapshots`)
|
|
37
|
+
console.log(`\nUse 'simba restore --snapshot <id>' to restore`)
|
|
38
|
+
},
|
|
39
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { ConfigStore } from "../core/config-store"
|
|
3
|
+
import { AgentRegistry } from "../core/agent-registry"
|
|
4
|
+
import { SkillManager } from "../core/skill-manager"
|
|
5
|
+
import { getConfigPath } from "../utils/paths"
|
|
6
|
+
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "status",
|
|
10
|
+
description: "Show skill matrix across agents",
|
|
11
|
+
},
|
|
12
|
+
args: {
|
|
13
|
+
agent: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Filter to specific agent",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
async run({ args }) {
|
|
19
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
20
|
+
const config = await configStore.load()
|
|
21
|
+
|
|
22
|
+
const registry = new AgentRegistry(config.agents)
|
|
23
|
+
const manager = new SkillManager(registry, config.agents)
|
|
24
|
+
|
|
25
|
+
const matrix = await manager.buildMatrix()
|
|
26
|
+
|
|
27
|
+
// Get detected agents
|
|
28
|
+
const detectedAgents = Object.entries(config.agents)
|
|
29
|
+
.filter(([_, a]) => a.detected)
|
|
30
|
+
.filter(([id]) => !args.agent || id === args.agent)
|
|
31
|
+
|
|
32
|
+
if (detectedAgents.length === 0) {
|
|
33
|
+
console.log("No agents detected. Run 'simba detect' first.")
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Print header
|
|
38
|
+
const agentNames = detectedAgents.map(([_, a]) => a.shortName.padEnd(8))
|
|
39
|
+
console.log(`\n${"Skill".padEnd(24)} ${agentNames.join(" ")}`)
|
|
40
|
+
console.log("─".repeat(24 + agentNames.length * 9))
|
|
41
|
+
|
|
42
|
+
// Print matrix
|
|
43
|
+
const statusSymbols = {
|
|
44
|
+
synced: "✓",
|
|
45
|
+
conflict: "⚠",
|
|
46
|
+
unique: "●",
|
|
47
|
+
missing: "─",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const row of matrix) {
|
|
51
|
+
const cells = detectedAgents.map(([id]) => {
|
|
52
|
+
const cell = row.agents[id]
|
|
53
|
+
if (!cell?.present) return "─".padStart(4).padEnd(8)
|
|
54
|
+
if (row.status === "conflict") return "⚠".padStart(4).padEnd(8)
|
|
55
|
+
return "✓".padStart(4).padEnd(8)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const skillName = row.skillName.slice(0, 23).padEnd(24)
|
|
59
|
+
console.log(`${skillName} ${cells.join(" ")}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Summary
|
|
63
|
+
const synced = matrix.filter((m) => m.status === "synced").length
|
|
64
|
+
const conflicts = matrix.filter((m) => m.status === "conflict").length
|
|
65
|
+
const unique = matrix.filter((m) => m.status === "unique").length
|
|
66
|
+
|
|
67
|
+
console.log("\n" + "─".repeat(24 + agentNames.length * 9))
|
|
68
|
+
console.log(`✓ synced: ${synced} ⚠ conflict: ${conflicts} ● unique: ${unique}`)
|
|
69
|
+
},
|
|
70
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { ConfigStore } from "../core/config-store"
|
|
3
|
+
import { AgentRegistry } from "../core/agent-registry"
|
|
4
|
+
import { SkillManager } from "../core/skill-manager"
|
|
5
|
+
import { SnapshotManager } from "../core/snapshot"
|
|
6
|
+
import { getConfigPath, getSnapshotsDir } from "../utils/paths"
|
|
7
|
+
import * as readline from "node:readline"
|
|
8
|
+
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "sync",
|
|
12
|
+
description: "Sync skills across agents (union merge)",
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
source: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Source of truth agent (one-way sync)",
|
|
18
|
+
},
|
|
19
|
+
dryRun: {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
alias: "n",
|
|
22
|
+
description: "Preview changes without applying",
|
|
23
|
+
default: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
async run({ args }) {
|
|
27
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
28
|
+
const config = await configStore.load()
|
|
29
|
+
|
|
30
|
+
const registry = new AgentRegistry(config.agents)
|
|
31
|
+
const manager = new SkillManager(registry, config.agents)
|
|
32
|
+
const snapshots = new SnapshotManager(
|
|
33
|
+
getSnapshotsDir(),
|
|
34
|
+
config.snapshots.maxCount
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const matrix = await manager.buildMatrix()
|
|
38
|
+
|
|
39
|
+
const unique = matrix.filter((m) => m.status === "unique")
|
|
40
|
+
const conflicts = matrix.filter((m) => m.status === "conflict")
|
|
41
|
+
|
|
42
|
+
if (unique.length === 0 && conflicts.length === 0) {
|
|
43
|
+
console.log("All skills are synced!")
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Show what will happen
|
|
48
|
+
if (unique.length > 0) {
|
|
49
|
+
console.log("\nWill copy:")
|
|
50
|
+
for (const skill of unique) {
|
|
51
|
+
const source = Object.entries(skill.agents).find(([_, v]) => v.present)?.[0]
|
|
52
|
+
const targets = Object.entries(skill.agents)
|
|
53
|
+
.filter(([_, v]) => !v.present)
|
|
54
|
+
.map(([id]) => id)
|
|
55
|
+
console.log(` ${skill.skillName} → ${targets.join(", ")}`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (conflicts.length > 0 && !args.source) {
|
|
60
|
+
console.log("\nConflicts (resolve manually):")
|
|
61
|
+
for (const skill of conflicts) {
|
|
62
|
+
const agents = Object.entries(skill.agents)
|
|
63
|
+
.filter(([_, v]) => v.present)
|
|
64
|
+
.map(([id]) => id)
|
|
65
|
+
console.log(` ${skill.skillName}: ${agents.join(" ≠ ")}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (args.dryRun) {
|
|
70
|
+
console.log("\n(dry run - no changes made)")
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create snapshot before changes
|
|
75
|
+
if (config.snapshots.autoSnapshot && (unique.length > 0 || conflicts.length > 0)) {
|
|
76
|
+
const skillPaths = [...unique, ...conflicts].flatMap((skill) =>
|
|
77
|
+
Object.entries(skill.agents)
|
|
78
|
+
.filter(([_, v]) => v.present)
|
|
79
|
+
.map(([agentId]) => registry.getSkillPath(skill.skillName, agentId))
|
|
80
|
+
)
|
|
81
|
+
await snapshots.createSnapshot(skillPaths, "pre-sync")
|
|
82
|
+
console.log("\nSnapshot created.")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Sync unique skills
|
|
86
|
+
for (const skill of unique) {
|
|
87
|
+
const source = Object.entries(skill.agents).find(([_, v]) => v.present)?.[0]
|
|
88
|
+
if (!source) continue
|
|
89
|
+
|
|
90
|
+
const synced = await manager.syncUnique(skill.skillName, source)
|
|
91
|
+
console.log(`Synced ${skill.skillName} to ${synced.join(", ")}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle conflicts with --source flag
|
|
95
|
+
if (args.source && conflicts.length > 0) {
|
|
96
|
+
for (const skill of conflicts) {
|
|
97
|
+
const losers = Object.entries(skill.agents)
|
|
98
|
+
.filter(([id, v]) => v.present && id !== args.source)
|
|
99
|
+
.map(([id]) => id)
|
|
100
|
+
|
|
101
|
+
await manager.resolveConflict(skill.skillName, args.source, losers)
|
|
102
|
+
console.log(`Resolved ${skill.skillName} using ${args.source}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log("\nSync complete!")
|
|
107
|
+
},
|
|
108
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { RegistryStore } from "../core/registry-store"
|
|
3
|
+
import { SkillsStore } from "../core/skills-store"
|
|
4
|
+
import { ConfigStore } from "../core/config-store"
|
|
5
|
+
import { getSkillsDir, getRegistryPath, getConfigPath, expandPath } from "../utils/paths"
|
|
6
|
+
|
|
7
|
+
export interface UnassignOptions {
|
|
8
|
+
skill: string
|
|
9
|
+
agents: string[]
|
|
10
|
+
skillsDir: string
|
|
11
|
+
registryPath: string
|
|
12
|
+
agentPaths: Record<string, string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runUnassign(options: UnassignOptions): Promise<void> {
|
|
16
|
+
const skillsStore = new SkillsStore(options.skillsDir, options.registryPath)
|
|
17
|
+
const registryStore = new RegistryStore(options.registryPath)
|
|
18
|
+
const registry = await registryStore.load()
|
|
19
|
+
|
|
20
|
+
const skill = registry.skills[options.skill]
|
|
21
|
+
if (!skill) {
|
|
22
|
+
console.error(`Skill not found: ${options.skill}`)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const agentId of options.agents) {
|
|
27
|
+
const agentPath = options.agentPaths[agentId]
|
|
28
|
+
if (!agentPath) {
|
|
29
|
+
console.error(`Unknown agent: ${agentId}`)
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await skillsStore.unassignSkill(options.skill, agentPath)
|
|
34
|
+
delete skill.assignments[agentId]
|
|
35
|
+
console.log(`Unassigned ${options.skill} from ${agentId}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await registryStore.save(registry)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default defineCommand({
|
|
42
|
+
meta: { name: "unassign", description: "Remove a skill from agents" },
|
|
43
|
+
args: {
|
|
44
|
+
skill: { type: "positional", description: "Skill name", required: true },
|
|
45
|
+
agents: { type: "positional", description: "Agent IDs (comma-separated)", required: true },
|
|
46
|
+
},
|
|
47
|
+
async run({ args }) {
|
|
48
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
49
|
+
const config = await configStore.load()
|
|
50
|
+
|
|
51
|
+
const agentPaths: Record<string, string> = {}
|
|
52
|
+
for (const [id, agent] of Object.entries(config.agents)) {
|
|
53
|
+
if (agent.detected) {
|
|
54
|
+
agentPaths[id] = expandPath(agent.globalPath)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const agents = (args.agents as string).split(",").map(a => a.trim())
|
|
59
|
+
|
|
60
|
+
await runUnassign({
|
|
61
|
+
skill: args.skill,
|
|
62
|
+
agents,
|
|
63
|
+
skillsDir: getSkillsDir(),
|
|
64
|
+
registryPath: getRegistryPath(),
|
|
65
|
+
agentPaths,
|
|
66
|
+
})
|
|
67
|
+
},
|
|
68
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { ConfigStore } from "../core/config-store"
|
|
3
|
+
import { SnapshotManager } from "../core/snapshot"
|
|
4
|
+
import { getConfigPath, getSnapshotsDir, expandPath } from "../utils/paths"
|
|
5
|
+
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "undo",
|
|
9
|
+
description: "Restore from most recent snapshot",
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
dryRun: {
|
|
13
|
+
type: "boolean",
|
|
14
|
+
alias: "n",
|
|
15
|
+
description: "Preview changes without applying",
|
|
16
|
+
default: false,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
async run({ args }) {
|
|
20
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
21
|
+
const config = await configStore.load()
|
|
22
|
+
|
|
23
|
+
const snapshots = new SnapshotManager(
|
|
24
|
+
getSnapshotsDir(),
|
|
25
|
+
config.snapshots.maxCount
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const latest = await snapshots.getLatestSnapshot()
|
|
29
|
+
|
|
30
|
+
if (!latest) {
|
|
31
|
+
console.log("No snapshots available.")
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`\nLatest snapshot: ${latest.id}`)
|
|
36
|
+
console.log(`Reason: ${latest.reason}`)
|
|
37
|
+
console.log(`Created: ${latest.created}`)
|
|
38
|
+
console.log(`Skills: ${latest.skills.join(", ")}`)
|
|
39
|
+
|
|
40
|
+
if (args.dryRun) {
|
|
41
|
+
console.log("\n(dry run - no changes made)")
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Restore to all detected agents
|
|
46
|
+
const targetAgents = Object.entries(config.agents)
|
|
47
|
+
.filter(([_, a]) => a.detected)
|
|
48
|
+
.map(([id, a]) => ({ id, path: expandPath(a.globalPath) }))
|
|
49
|
+
|
|
50
|
+
for (const { id, path } of targetAgents) {
|
|
51
|
+
await snapshots.restore(latest.id, path)
|
|
52
|
+
console.log(`Restored to ${config.agents[id].name}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log("\nUndo complete!")
|
|
56
|
+
},
|
|
57
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import { rm } from "node:fs/promises"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import * as p from "@clack/prompts"
|
|
5
|
+
import { RegistryStore } from "../core/registry-store"
|
|
6
|
+
import { SkillsStore } from "../core/skills-store"
|
|
7
|
+
import { ConfigStore } from "../core/config-store"
|
|
8
|
+
import { AgentRegistry } from "../core/agent-registry"
|
|
9
|
+
import { getSkillsDir, getRegistryPath, getConfigPath, expandPath } from "../utils/paths"
|
|
10
|
+
|
|
11
|
+
export interface UninstallOptions {
|
|
12
|
+
skills: string[]
|
|
13
|
+
skillsDir: string
|
|
14
|
+
registryPath: string
|
|
15
|
+
agentPaths: Record<string, string>
|
|
16
|
+
deleteFiles: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runUninstall(options: UninstallOptions): Promise<void> {
|
|
20
|
+
const skillsStore = new SkillsStore(options.skillsDir, options.registryPath)
|
|
21
|
+
const registryStore = new RegistryStore(options.registryPath)
|
|
22
|
+
const registry = await registryStore.load()
|
|
23
|
+
|
|
24
|
+
for (const name of options.skills) {
|
|
25
|
+
const skill = registry.skills[name]
|
|
26
|
+
if (!skill) {
|
|
27
|
+
console.log(` Skill not found: ${name}`)
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Remove symlinks from all assigned agents
|
|
32
|
+
for (const agentId of Object.keys(skill.assignments)) {
|
|
33
|
+
const agentPath = options.agentPaths[agentId]
|
|
34
|
+
if (agentPath) {
|
|
35
|
+
try {
|
|
36
|
+
await rm(join(agentPath, name), { recursive: true, force: true })
|
|
37
|
+
console.log(` Removed from ${agentId}`)
|
|
38
|
+
} catch {
|
|
39
|
+
// Ignore errors if symlink doesn't exist
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Remove from store if requested
|
|
45
|
+
if (options.deleteFiles) {
|
|
46
|
+
await rm(join(options.skillsDir, name), { recursive: true, force: true })
|
|
47
|
+
console.log(` Deleted files: ${name}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Remove from registry
|
|
51
|
+
delete registry.skills[name]
|
|
52
|
+
|
|
53
|
+
console.log(` Uninstalled: ${name}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await registryStore.save(registry)
|
|
57
|
+
console.log("\nUninstall complete!")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default defineCommand({
|
|
61
|
+
meta: { name: "uninstall", description: "Remove a skill from Simba's store" },
|
|
62
|
+
args: {
|
|
63
|
+
skill: { type: "positional", description: "Skill name to uninstall", required: false },
|
|
64
|
+
},
|
|
65
|
+
async run({ args }) {
|
|
66
|
+
const registryStore = new RegistryStore(getRegistryPath())
|
|
67
|
+
const registry = await registryStore.load()
|
|
68
|
+
|
|
69
|
+
const configStore = new ConfigStore(getConfigPath())
|
|
70
|
+
const config = await configStore.load()
|
|
71
|
+
|
|
72
|
+
// Get agent paths
|
|
73
|
+
const agentRegistry = new AgentRegistry(config.agents)
|
|
74
|
+
const detected = await agentRegistry.detectAgents()
|
|
75
|
+
|
|
76
|
+
const agentPaths: Record<string, string> = {}
|
|
77
|
+
for (const [id, agent] of Object.entries(detected)) {
|
|
78
|
+
agentPaths[id] = expandPath(agent.globalPath)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let skills: string[]
|
|
82
|
+
|
|
83
|
+
if (args.skill) {
|
|
84
|
+
skills = [args.skill as string]
|
|
85
|
+
} else {
|
|
86
|
+
// Interactive mode
|
|
87
|
+
const managedSkills = Object.keys(registry.skills)
|
|
88
|
+
|
|
89
|
+
if (managedSkills.length === 0) {
|
|
90
|
+
console.log("No skills managed.")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const result = await p.multiselect({
|
|
95
|
+
message: "Select skills to uninstall",
|
|
96
|
+
options: managedSkills.map((s) => ({ value: s, label: s })),
|
|
97
|
+
required: true,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (p.isCancel(result)) process.exit(0)
|
|
101
|
+
skills = result as string[]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Confirm
|
|
105
|
+
const confirm = await p.confirm({
|
|
106
|
+
message: `Uninstall ${skills.length} skill(s)? This will remove them from all agents.`,
|
|
107
|
+
initialValue: false,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
111
|
+
console.log("Cancelled.")
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Ask about deleting files
|
|
116
|
+
const deleteFiles = await p.confirm({
|
|
117
|
+
message: "Also delete skill files from Simba's store?",
|
|
118
|
+
initialValue: true,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if (p.isCancel(deleteFiles)) {
|
|
122
|
+
console.log("Cancelled.")
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await runUninstall({
|
|
127
|
+
skills,
|
|
128
|
+
skillsDir: getSkillsDir(),
|
|
129
|
+
registryPath: getRegistryPath(),
|
|
130
|
+
agentPaths,
|
|
131
|
+
deleteFiles: deleteFiles as boolean,
|
|
132
|
+
})
|
|
133
|
+
},
|
|
134
|
+
})
|