@cremini/skillpack 1.1.7 → 1.1.8-beta.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 +19 -11
- package/dist/cli.js +998 -132
- package/dist/runtime/registry.js +244 -0
- package/package.json +4 -2
- package/web/js/api-key-dialog.js +3 -5
- package/web/js/chat-apps-dialog.js +4 -10
- package/web/js/settings.js +3 -8
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/runtime/registry.ts
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import path from "path";
|
|
8
|
+
var SKILLPACK_HOME = path.join(os.homedir(), ".skillpack");
|
|
9
|
+
var LEGACY_REGISTRY_FILE = path.join(SKILLPACK_HOME, "registry.json");
|
|
10
|
+
var REGISTRY_DIR = path.join(SKILLPACK_HOME, "registry.d");
|
|
11
|
+
var migrationChecked = false;
|
|
12
|
+
function getRegistryPath() {
|
|
13
|
+
ensureRegistryReady();
|
|
14
|
+
return REGISTRY_DIR;
|
|
15
|
+
}
|
|
16
|
+
function ensureHomeDir() {
|
|
17
|
+
if (!fs.existsSync(SKILLPACK_HOME)) {
|
|
18
|
+
fs.mkdirSync(SKILLPACK_HOME, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function ensureRegistryDir() {
|
|
22
|
+
ensureHomeDir();
|
|
23
|
+
if (!fs.existsSync(REGISTRY_DIR)) {
|
|
24
|
+
fs.mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function canonicalizeDir(dir) {
|
|
28
|
+
const resolved = path.resolve(dir);
|
|
29
|
+
try {
|
|
30
|
+
return fs.realpathSync(resolved);
|
|
31
|
+
} catch {
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function hashDir(dir) {
|
|
36
|
+
return crypto.createHash("md5").update(canonicalizeDir(dir)).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
function getEntryPathForCanonicalDir(dir) {
|
|
39
|
+
return path.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
|
|
40
|
+
}
|
|
41
|
+
function getEntryPath(dir) {
|
|
42
|
+
ensureRegistryReady();
|
|
43
|
+
return getEntryPathForCanonicalDir(canonicalizeDir(dir));
|
|
44
|
+
}
|
|
45
|
+
function listEntryFiles() {
|
|
46
|
+
ensureRegistryReady();
|
|
47
|
+
return fs.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path.join(REGISTRY_DIR, file));
|
|
48
|
+
}
|
|
49
|
+
function readEntryFile(filePath) {
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
52
|
+
const data = JSON.parse(raw);
|
|
53
|
+
if (typeof data?.dir !== "string" || typeof data?.name !== "string" || typeof data?.version !== "string" || typeof data?.port !== "number" || typeof data?.pid !== "number" && data?.pid !== null || data?.status !== "running" && data?.status !== "stopped") {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
dir: canonicalizeDir(data.dir),
|
|
58
|
+
name: data.name,
|
|
59
|
+
version: data.version,
|
|
60
|
+
port: data.port,
|
|
61
|
+
pid: data.pid,
|
|
62
|
+
status: data.status,
|
|
63
|
+
startedAt: data.startedAt,
|
|
64
|
+
stoppedAt: data.stoppedAt,
|
|
65
|
+
updatedAt: data.updatedAt
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function createTmpPath(entryPath) {
|
|
72
|
+
const suffix = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
73
|
+
return `${entryPath}.tmp.${suffix}`;
|
|
74
|
+
}
|
|
75
|
+
function writeEntryFile(entry) {
|
|
76
|
+
ensureRegistryReady();
|
|
77
|
+
const normalized = {
|
|
78
|
+
...entry,
|
|
79
|
+
dir: canonicalizeDir(entry.dir),
|
|
80
|
+
updatedAt: entry.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
81
|
+
};
|
|
82
|
+
const entryPath = getEntryPathForCanonicalDir(normalized.dir);
|
|
83
|
+
const tmpPath = createTmpPath(entryPath);
|
|
84
|
+
fs.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
|
|
85
|
+
fs.renameSync(tmpPath, entryPath);
|
|
86
|
+
}
|
|
87
|
+
function migrateLegacyRegistryIfNeeded() {
|
|
88
|
+
if (migrationChecked) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
migrationChecked = true;
|
|
92
|
+
ensureRegistryDir();
|
|
93
|
+
if (!fs.existsSync(LEGACY_REGISTRY_FILE)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (listEntryFiles().length > 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const raw = fs.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
|
|
101
|
+
const data = JSON.parse(raw);
|
|
102
|
+
const packs = Array.isArray(data?.packs) ? data.packs : [];
|
|
103
|
+
for (const pack of packs) {
|
|
104
|
+
try {
|
|
105
|
+
writeEntryFile({
|
|
106
|
+
...pack,
|
|
107
|
+
dir: canonicalizeDir(pack.dir),
|
|
108
|
+
updatedAt: pack.updatedAt ?? pack.stoppedAt ?? pack.startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
fs.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.warn(" [Registry] Failed to migrate legacy registry.json:", err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function ensureRegistryReady() {
|
|
119
|
+
ensureRegistryDir();
|
|
120
|
+
migrateLegacyRegistryIfNeeded();
|
|
121
|
+
}
|
|
122
|
+
function entriesEqual(a, b) {
|
|
123
|
+
return a.dir === b.dir && a.name === b.name && a.version === b.version && a.port === b.port && a.pid === b.pid && a.status === b.status && a.startedAt === b.startedAt && a.stoppedAt === b.stoppedAt;
|
|
124
|
+
}
|
|
125
|
+
function readEntry(dir) {
|
|
126
|
+
ensureRegistryReady();
|
|
127
|
+
return readEntryFile(getEntryPath(dir));
|
|
128
|
+
}
|
|
129
|
+
function writeEntry(entry) {
|
|
130
|
+
writeEntryFile(entry);
|
|
131
|
+
}
|
|
132
|
+
function deleteEntry(dir) {
|
|
133
|
+
ensureRegistryReady();
|
|
134
|
+
const entryPath = getEntryPath(dir);
|
|
135
|
+
if (fs.existsSync(entryPath)) {
|
|
136
|
+
fs.unlinkSync(entryPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function readRegistry() {
|
|
140
|
+
return { packs: readAll() };
|
|
141
|
+
}
|
|
142
|
+
function writeRegistry(data) {
|
|
143
|
+
ensureRegistryReady();
|
|
144
|
+
const nextPaths = /* @__PURE__ */ new Set();
|
|
145
|
+
for (const pack of data.packs) {
|
|
146
|
+
const normalized = {
|
|
147
|
+
...pack,
|
|
148
|
+
dir: canonicalizeDir(pack.dir),
|
|
149
|
+
updatedAt: pack.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
150
|
+
};
|
|
151
|
+
const entryPath = getEntryPathForCanonicalDir(normalized.dir);
|
|
152
|
+
nextPaths.add(entryPath);
|
|
153
|
+
writeEntryFile(normalized);
|
|
154
|
+
}
|
|
155
|
+
for (const existingPath of listEntryFiles()) {
|
|
156
|
+
if (!nextPaths.has(existingPath)) {
|
|
157
|
+
fs.unlinkSync(existingPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function register(opts) {
|
|
162
|
+
try {
|
|
163
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
164
|
+
const entry = {
|
|
165
|
+
dir: canonicalizeDir(opts.dir),
|
|
166
|
+
name: opts.name,
|
|
167
|
+
version: opts.version,
|
|
168
|
+
port: opts.port,
|
|
169
|
+
pid: process.pid,
|
|
170
|
+
status: "running",
|
|
171
|
+
startedAt: now,
|
|
172
|
+
updatedAt: now
|
|
173
|
+
};
|
|
174
|
+
writeEntryFile(entry);
|
|
175
|
+
console.log(` [Registry] Registered "${opts.name}" (pid ${process.pid})`);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.warn(" [Registry] Failed to register:", err);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function deregister(dir, pid) {
|
|
181
|
+
try {
|
|
182
|
+
const entry = readEntry(dir);
|
|
183
|
+
if (!entry || entry.pid !== pid) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
187
|
+
writeEntryFile({
|
|
188
|
+
...entry,
|
|
189
|
+
pid: null,
|
|
190
|
+
status: "stopped",
|
|
191
|
+
stoppedAt: now,
|
|
192
|
+
updatedAt: now
|
|
193
|
+
});
|
|
194
|
+
console.log(` [Registry] Deregistered "${entry.name}"`);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.warn(" [Registry] Failed to deregister:", err);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function readAll() {
|
|
200
|
+
return listEntryFiles().map((entryPath) => readEntryFile(entryPath)).filter((entry) => entry !== null);
|
|
201
|
+
}
|
|
202
|
+
function isPidAlive(pid) {
|
|
203
|
+
try {
|
|
204
|
+
process.kill(pid, 0);
|
|
205
|
+
return true;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function validateEntries() {
|
|
211
|
+
const entries = readAll();
|
|
212
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
if (entry.status === "running" && entry.pid !== null && !isPidAlive(entry.pid)) {
|
|
215
|
+
writeEntryFile({
|
|
216
|
+
...entry,
|
|
217
|
+
pid: null,
|
|
218
|
+
status: "stopped",
|
|
219
|
+
stoppedAt: now,
|
|
220
|
+
updatedAt: now
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const nextEntries = readAll();
|
|
225
|
+
if (entries.length === nextEntries.length && entries.every((entry, index) => entriesEqual(entry, nextEntries[index]))) {
|
|
226
|
+
return entries;
|
|
227
|
+
}
|
|
228
|
+
return nextEntries;
|
|
229
|
+
}
|
|
230
|
+
export {
|
|
231
|
+
canonicalizeDir,
|
|
232
|
+
deleteEntry,
|
|
233
|
+
deregister,
|
|
234
|
+
getEntryPath,
|
|
235
|
+
getRegistryPath,
|
|
236
|
+
isPidAlive,
|
|
237
|
+
readAll,
|
|
238
|
+
readEntry,
|
|
239
|
+
readRegistry,
|
|
240
|
+
register,
|
|
241
|
+
validateEntries,
|
|
242
|
+
writeEntry,
|
|
243
|
+
writeRegistry
|
|
244
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cremini/skillpack",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8-beta.1",
|
|
4
4
|
"description": "Pack AI Skills into Local Agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"commander": "^14.0.3",
|
|
50
50
|
"express": "^5.1.0",
|
|
51
51
|
"inquirer": "^13.3.0",
|
|
52
|
+
"node-cron": "^4.2.1",
|
|
52
53
|
"node-telegram-bot-api": "^0.66.0",
|
|
53
54
|
"ws": "^8.19.0"
|
|
54
55
|
},
|
|
@@ -57,10 +58,11 @@
|
|
|
57
58
|
"@types/express": "^5.0.0",
|
|
58
59
|
"@types/inquirer": "^9.0.9",
|
|
59
60
|
"@types/node": "^25.5.0",
|
|
61
|
+
"@types/node-cron": "^3.0.11",
|
|
60
62
|
"@types/node-telegram-bot-api": "^0.64.0",
|
|
61
63
|
"@types/ws": "^8.18.0",
|
|
62
64
|
"prettier": "^3.8.1",
|
|
63
65
|
"tsup": "^8.5.1",
|
|
64
66
|
"typescript": "^5.9.3"
|
|
65
67
|
}
|
|
66
|
-
}
|
|
68
|
+
}
|
package/web/js/api-key-dialog.js
CHANGED
|
@@ -136,19 +136,17 @@ async function handleSave() {
|
|
|
136
136
|
apiKeyInput.value = "";
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
state.restartRequired = !!res.requiresRestart;
|
|
141
141
|
|
|
142
142
|
updateApiKeyButton();
|
|
143
143
|
|
|
144
144
|
if (res.requiresRestart) {
|
|
145
145
|
setStatus(
|
|
146
|
-
|
|
147
|
-
? "API key saved. Restart service to apply changes."
|
|
148
|
-
: "API key saved. Restart the service manually to apply changes.",
|
|
146
|
+
"API key saved. Restart service to apply changes.",
|
|
149
147
|
"warning",
|
|
150
148
|
);
|
|
151
|
-
updateRestartButton(
|
|
149
|
+
updateRestartButton(true);
|
|
152
150
|
} else {
|
|
153
151
|
setStatus("API key saved successfully", "success");
|
|
154
152
|
// 延迟关闭让用户看到成功消息
|
|
@@ -101,14 +101,11 @@ function populateForm() {
|
|
|
101
101
|
|
|
102
102
|
// Restart required status
|
|
103
103
|
if (state.restartRequired) {
|
|
104
|
-
const canRestart = config.runtimeControl?.canManagedRestart;
|
|
105
104
|
setStatus(
|
|
106
|
-
|
|
107
|
-
? "Settings changed. Restart service to apply."
|
|
108
|
-
: "Settings changed. Restart the service manually to apply.",
|
|
105
|
+
"Settings changed. Restart service to apply.",
|
|
109
106
|
"warning",
|
|
110
107
|
);
|
|
111
|
-
updateRestartButton(
|
|
108
|
+
updateRestartButton(true);
|
|
112
109
|
} else {
|
|
113
110
|
setStatus("", "");
|
|
114
111
|
updateRestartButton(false);
|
|
@@ -139,17 +136,14 @@ async function handleSave() {
|
|
|
139
136
|
const res = await saveConfigData(updates);
|
|
140
137
|
|
|
141
138
|
state.config.adapters = res.adapters;
|
|
142
|
-
state.config.runtimeControl = res.runtimeControl;
|
|
143
139
|
state.restartRequired = !!res.requiresRestart;
|
|
144
140
|
|
|
145
141
|
if (res.requiresRestart) {
|
|
146
142
|
setStatus(
|
|
147
|
-
|
|
148
|
-
? "Settings saved. Restart service to apply changes."
|
|
149
|
-
: "Settings saved. Restart the service manually to apply changes.",
|
|
143
|
+
"Settings saved. Restart service to apply changes.",
|
|
150
144
|
"warning",
|
|
151
145
|
);
|
|
152
|
-
updateRestartButton(
|
|
146
|
+
updateRestartButton(true);
|
|
153
147
|
} else {
|
|
154
148
|
close();
|
|
155
149
|
}
|
package/web/js/settings.js
CHANGED
|
@@ -72,9 +72,7 @@ function populateForm() {
|
|
|
72
72
|
|
|
73
73
|
if (state.restartRequired) {
|
|
74
74
|
setStatus(
|
|
75
|
-
|
|
76
|
-
? "Settings saved. Restart service to apply changes."
|
|
77
|
-
: "Settings saved. Restart the service manually to apply changes.",
|
|
75
|
+
"Settings saved. Restart service to apply changes.",
|
|
78
76
|
"warning",
|
|
79
77
|
);
|
|
80
78
|
updateRestartButton(true);
|
|
@@ -139,7 +137,6 @@ async function handleSave() {
|
|
|
139
137
|
// Update local config
|
|
140
138
|
state.config.provider = res.provider;
|
|
141
139
|
state.config.adapters = res.adapters;
|
|
142
|
-
state.config.runtimeControl = res.runtimeControl;
|
|
143
140
|
if (updates.key) {
|
|
144
141
|
state.config.hasApiKey = true;
|
|
145
142
|
state.config.apiKey = updates.key;
|
|
@@ -156,12 +153,10 @@ async function handleSave() {
|
|
|
156
153
|
|
|
157
154
|
if (res.requiresRestart) {
|
|
158
155
|
setStatus(
|
|
159
|
-
|
|
160
|
-
? "Settings saved. Restart service to apply changes."
|
|
161
|
-
: "Settings saved. Restart the service manually to apply changes.",
|
|
156
|
+
"Settings saved. Restart service to apply changes.",
|
|
162
157
|
"warning",
|
|
163
158
|
);
|
|
164
|
-
updateRestartButton(
|
|
159
|
+
updateRestartButton(true);
|
|
165
160
|
return;
|
|
166
161
|
}
|
|
167
162
|
|