@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.
@@ -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.7",
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
+ }
@@ -136,19 +136,17 @@ async function handleSave() {
136
136
  apiKeyInput.value = "";
137
137
  }
138
138
 
139
- state.config.runtimeControl = res.runtimeControl;
139
+
140
140
  state.restartRequired = !!res.requiresRestart;
141
141
 
142
142
  updateApiKeyButton();
143
143
 
144
144
  if (res.requiresRestart) {
145
145
  setStatus(
146
- res.runtimeControl?.canManagedRestart
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(!!res.runtimeControl?.canManagedRestart);
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
- canRestart
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(canRestart);
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
- res.runtimeControl?.canManagedRestart
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(!!res.runtimeControl?.canManagedRestart);
146
+ updateRestartButton(true);
153
147
  } else {
154
148
  close();
155
149
  }
@@ -72,9 +72,7 @@ function populateForm() {
72
72
 
73
73
  if (state.restartRequired) {
74
74
  setStatus(
75
- config.runtimeControl?.canManagedRestart
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
- res.runtimeControl.canManagedRestart
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(res.runtimeControl.canManagedRestart);
159
+ updateRestartButton(true);
165
160
  return;
166
161
  }
167
162