@cremini/skillpack 1.1.9 → 1.2.0
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/dist/cli.js +217 -52
- package/dist/runtime/registry.js +244 -0
- package/package.json +2 -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
package/dist/cli.js
CHANGED
|
@@ -293,7 +293,7 @@ var telegram_exports = {};
|
|
|
293
293
|
__export(telegram_exports, {
|
|
294
294
|
TelegramAdapter: () => TelegramAdapter
|
|
295
295
|
});
|
|
296
|
-
import
|
|
296
|
+
import fs11 from "fs";
|
|
297
297
|
import TelegramBot from "node-telegram-bot-api";
|
|
298
298
|
var COMMANDS2, MAX_MESSAGE_LENGTH, ACK_REACTION, TelegramAdapter;
|
|
299
299
|
var init_telegram = __esm({
|
|
@@ -604,7 +604,7 @@ var init_telegram = __esm({
|
|
|
604
604
|
async sendFileSafe(chatId, filePath, caption) {
|
|
605
605
|
if (!this.bot) return;
|
|
606
606
|
try {
|
|
607
|
-
if (!
|
|
607
|
+
if (!fs11.existsSync(filePath)) {
|
|
608
608
|
console.error(`[Telegram] File not found for sending: ${filePath}`);
|
|
609
609
|
return;
|
|
610
610
|
}
|
|
@@ -624,8 +624,8 @@ var slack_exports = {};
|
|
|
624
624
|
__export(slack_exports, {
|
|
625
625
|
SlackAdapter: () => SlackAdapter
|
|
626
626
|
});
|
|
627
|
-
import
|
|
628
|
-
import
|
|
627
|
+
import fs12 from "fs";
|
|
628
|
+
import path11 from "path";
|
|
629
629
|
import { App, LogLevel } from "@slack/bolt";
|
|
630
630
|
var INLINE_COMMANDS, SLASH_COMMANDS, MAX_MESSAGE_LENGTH2, ACK_REACTION2, SlackAdapter;
|
|
631
631
|
var init_slack = __esm({
|
|
@@ -1103,12 +1103,12 @@ var init_slack = __esm({
|
|
|
1103
1103
|
*/
|
|
1104
1104
|
async sendFileSafe(client, route, filePath, caption) {
|
|
1105
1105
|
try {
|
|
1106
|
-
if (!
|
|
1106
|
+
if (!fs12.existsSync(filePath)) {
|
|
1107
1107
|
console.error(`[Slack] File not found for sending: ${filePath}`);
|
|
1108
1108
|
return;
|
|
1109
1109
|
}
|
|
1110
|
-
const filename =
|
|
1111
|
-
const fileContent =
|
|
1110
|
+
const filename = path11.basename(filePath);
|
|
1111
|
+
const fileContent = fs12.readFileSync(filePath);
|
|
1112
1112
|
await client.files.uploadV2({
|
|
1113
1113
|
channel_id: route.channel,
|
|
1114
1114
|
thread_ts: route.threadTs,
|
|
@@ -2043,15 +2043,15 @@ async function interactiveCreate(workDir) {
|
|
|
2043
2043
|
}
|
|
2044
2044
|
|
|
2045
2045
|
// src/commands/run.ts
|
|
2046
|
-
import
|
|
2047
|
-
import
|
|
2046
|
+
import path13 from "path";
|
|
2047
|
+
import fs14 from "fs";
|
|
2048
2048
|
import inquirer2 from "inquirer";
|
|
2049
2049
|
import chalk4 from "chalk";
|
|
2050
2050
|
|
|
2051
2051
|
// src/runtime/server.ts
|
|
2052
2052
|
import express from "express";
|
|
2053
|
-
import
|
|
2054
|
-
import
|
|
2053
|
+
import path12 from "path";
|
|
2054
|
+
import fs13 from "fs";
|
|
2055
2055
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2056
2056
|
import { createServer } from "http";
|
|
2057
2057
|
import { exec } from "child_process";
|
|
@@ -5349,8 +5349,7 @@ var WebAdapter = class {
|
|
|
5349
5349
|
apiKey: conf.apiKey || "",
|
|
5350
5350
|
provider: conf.provider || "openai",
|
|
5351
5351
|
baseUrl: conf.baseUrl || "",
|
|
5352
|
-
adapters: conf.adapters || {}
|
|
5353
|
-
runtimeControl: lifecycle.getRuntimeControl()
|
|
5352
|
+
adapters: conf.adapters || {}
|
|
5354
5353
|
});
|
|
5355
5354
|
});
|
|
5356
5355
|
app.get("/api/skills", (_req, res) => {
|
|
@@ -5383,22 +5382,12 @@ var WebAdapter = class {
|
|
|
5383
5382
|
provider: newConf.provider,
|
|
5384
5383
|
baseUrl: newConf.baseUrl || "",
|
|
5385
5384
|
adapters: newConf.adapters,
|
|
5386
|
-
requiresRestart
|
|
5387
|
-
runtimeControl: lifecycle.getRuntimeControl()
|
|
5385
|
+
requiresRestart
|
|
5388
5386
|
});
|
|
5389
5387
|
});
|
|
5390
5388
|
app.post("/api/runtime/restart", async (_req, res) => {
|
|
5391
|
-
const runtimeControl = lifecycle.getRuntimeControl();
|
|
5392
|
-
if (!runtimeControl.canManagedRestart) {
|
|
5393
|
-
res.status(409).json({
|
|
5394
|
-
success: false,
|
|
5395
|
-
message: "Managed restart is unavailable for this process.",
|
|
5396
|
-
runtimeControl
|
|
5397
|
-
});
|
|
5398
|
-
return;
|
|
5399
|
-
}
|
|
5400
5389
|
const result = await lifecycle.requestRestart("web");
|
|
5401
|
-
res.status(202).json(
|
|
5390
|
+
res.status(202).json(result);
|
|
5402
5391
|
});
|
|
5403
5392
|
app.delete("/api/chat", (_req, res) => {
|
|
5404
5393
|
res.json({ success: true });
|
|
@@ -5590,29 +5579,18 @@ init_config();
|
|
|
5590
5579
|
var SHUTDOWN_EXIT_CODE = 64;
|
|
5591
5580
|
var RESTART_EXIT_CODE = 75;
|
|
5592
5581
|
var STOP_TIMEOUT_MS = 3e3;
|
|
5593
|
-
function detectProcessManager() {
|
|
5594
|
-
return process.env.PACK_ROOT ? "wrapper" : "none";
|
|
5595
|
-
}
|
|
5596
5582
|
var Lifecycle = class {
|
|
5597
5583
|
server;
|
|
5598
5584
|
exitFn;
|
|
5599
|
-
processManager;
|
|
5600
5585
|
adapters = [];
|
|
5601
5586
|
stopReason = null;
|
|
5602
5587
|
constructor(server, exitFn = (code) => process.exit(code)) {
|
|
5603
5588
|
this.server = server;
|
|
5604
5589
|
this.exitFn = exitFn;
|
|
5605
|
-
this.processManager = detectProcessManager();
|
|
5606
5590
|
}
|
|
5607
5591
|
registerAdapters(adapters) {
|
|
5608
5592
|
this.adapters = adapters;
|
|
5609
5593
|
}
|
|
5610
|
-
getRuntimeControl() {
|
|
5611
|
-
return {
|
|
5612
|
-
canManagedRestart: this.processManager === "wrapper",
|
|
5613
|
-
processManager: this.processManager
|
|
5614
|
-
};
|
|
5615
|
-
}
|
|
5616
5594
|
async requestRestart(trigger) {
|
|
5617
5595
|
return this.requestStop("restart", trigger);
|
|
5618
5596
|
}
|
|
@@ -5668,26 +5646,198 @@ var Lifecycle = class {
|
|
|
5668
5646
|
}
|
|
5669
5647
|
};
|
|
5670
5648
|
|
|
5649
|
+
// src/runtime/registry.ts
|
|
5650
|
+
import crypto from "crypto";
|
|
5651
|
+
import fs10 from "fs";
|
|
5652
|
+
import os from "os";
|
|
5653
|
+
import path10 from "path";
|
|
5654
|
+
var SKILLPACK_HOME = path10.join(os.homedir(), ".skillpack");
|
|
5655
|
+
var LEGACY_REGISTRY_FILE = path10.join(SKILLPACK_HOME, "registry.json");
|
|
5656
|
+
var REGISTRY_DIR = path10.join(SKILLPACK_HOME, "registry.d");
|
|
5657
|
+
var migrationChecked = false;
|
|
5658
|
+
function ensureHomeDir() {
|
|
5659
|
+
if (!fs10.existsSync(SKILLPACK_HOME)) {
|
|
5660
|
+
fs10.mkdirSync(SKILLPACK_HOME, { recursive: true });
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
function ensureRegistryDir() {
|
|
5664
|
+
ensureHomeDir();
|
|
5665
|
+
if (!fs10.existsSync(REGISTRY_DIR)) {
|
|
5666
|
+
fs10.mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
function canonicalizeDir(dir) {
|
|
5670
|
+
const resolved = path10.resolve(dir);
|
|
5671
|
+
try {
|
|
5672
|
+
return fs10.realpathSync(resolved);
|
|
5673
|
+
} catch {
|
|
5674
|
+
return resolved;
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
function hashDir(dir) {
|
|
5678
|
+
return crypto.createHash("md5").update(canonicalizeDir(dir)).digest("hex");
|
|
5679
|
+
}
|
|
5680
|
+
function getEntryPathForCanonicalDir(dir) {
|
|
5681
|
+
return path10.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
|
|
5682
|
+
}
|
|
5683
|
+
function getEntryPath(dir) {
|
|
5684
|
+
ensureRegistryReady();
|
|
5685
|
+
return getEntryPathForCanonicalDir(canonicalizeDir(dir));
|
|
5686
|
+
}
|
|
5687
|
+
function listEntryFiles() {
|
|
5688
|
+
ensureRegistryReady();
|
|
5689
|
+
return fs10.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path10.join(REGISTRY_DIR, file));
|
|
5690
|
+
}
|
|
5691
|
+
function readEntryFile(filePath) {
|
|
5692
|
+
try {
|
|
5693
|
+
const raw = fs10.readFileSync(filePath, "utf-8");
|
|
5694
|
+
const data = JSON.parse(raw);
|
|
5695
|
+
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") {
|
|
5696
|
+
return null;
|
|
5697
|
+
}
|
|
5698
|
+
return {
|
|
5699
|
+
dir: canonicalizeDir(data.dir),
|
|
5700
|
+
name: data.name,
|
|
5701
|
+
version: data.version,
|
|
5702
|
+
port: data.port,
|
|
5703
|
+
pid: data.pid,
|
|
5704
|
+
status: data.status,
|
|
5705
|
+
startedAt: data.startedAt,
|
|
5706
|
+
stoppedAt: data.stoppedAt,
|
|
5707
|
+
updatedAt: data.updatedAt
|
|
5708
|
+
};
|
|
5709
|
+
} catch {
|
|
5710
|
+
return null;
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
function createTmpPath(entryPath) {
|
|
5714
|
+
const suffix = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
5715
|
+
return `${entryPath}.tmp.${suffix}`;
|
|
5716
|
+
}
|
|
5717
|
+
function writeEntryFile(entry) {
|
|
5718
|
+
ensureRegistryReady();
|
|
5719
|
+
const normalized = {
|
|
5720
|
+
...entry,
|
|
5721
|
+
dir: canonicalizeDir(entry.dir),
|
|
5722
|
+
updatedAt: entry.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
5723
|
+
};
|
|
5724
|
+
const entryPath = getEntryPathForCanonicalDir(normalized.dir);
|
|
5725
|
+
const tmpPath = createTmpPath(entryPath);
|
|
5726
|
+
fs10.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
|
|
5727
|
+
fs10.renameSync(tmpPath, entryPath);
|
|
5728
|
+
}
|
|
5729
|
+
function migrateLegacyRegistryIfNeeded() {
|
|
5730
|
+
if (migrationChecked) {
|
|
5731
|
+
return;
|
|
5732
|
+
}
|
|
5733
|
+
migrationChecked = true;
|
|
5734
|
+
ensureRegistryDir();
|
|
5735
|
+
if (!fs10.existsSync(LEGACY_REGISTRY_FILE)) {
|
|
5736
|
+
return;
|
|
5737
|
+
}
|
|
5738
|
+
if (listEntryFiles().length > 0) {
|
|
5739
|
+
return;
|
|
5740
|
+
}
|
|
5741
|
+
try {
|
|
5742
|
+
const raw = fs10.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
|
|
5743
|
+
const data = JSON.parse(raw);
|
|
5744
|
+
const packs = Array.isArray(data?.packs) ? data.packs : [];
|
|
5745
|
+
for (const pack of packs) {
|
|
5746
|
+
try {
|
|
5747
|
+
writeEntryFile({
|
|
5748
|
+
...pack,
|
|
5749
|
+
dir: canonicalizeDir(pack.dir),
|
|
5750
|
+
updatedAt: pack.updatedAt ?? pack.stoppedAt ?? pack.startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
5751
|
+
});
|
|
5752
|
+
} catch {
|
|
5753
|
+
}
|
|
5754
|
+
}
|
|
5755
|
+
fs10.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
|
|
5756
|
+
} catch (err) {
|
|
5757
|
+
console.warn(" [Registry] Failed to migrate legacy registry.json:", err);
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
function ensureRegistryReady() {
|
|
5761
|
+
ensureRegistryDir();
|
|
5762
|
+
migrateLegacyRegistryIfNeeded();
|
|
5763
|
+
}
|
|
5764
|
+
function readEntry(dir) {
|
|
5765
|
+
ensureRegistryReady();
|
|
5766
|
+
return readEntryFile(getEntryPath(dir));
|
|
5767
|
+
}
|
|
5768
|
+
function register(opts) {
|
|
5769
|
+
try {
|
|
5770
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5771
|
+
const entry = {
|
|
5772
|
+
dir: canonicalizeDir(opts.dir),
|
|
5773
|
+
name: opts.name,
|
|
5774
|
+
version: opts.version,
|
|
5775
|
+
port: opts.port,
|
|
5776
|
+
pid: process.pid,
|
|
5777
|
+
status: "running",
|
|
5778
|
+
startedAt: now,
|
|
5779
|
+
updatedAt: now
|
|
5780
|
+
};
|
|
5781
|
+
writeEntryFile(entry);
|
|
5782
|
+
console.log(` [Registry] Registered "${opts.name}" (pid ${process.pid})`);
|
|
5783
|
+
} catch (err) {
|
|
5784
|
+
console.warn(" [Registry] Failed to register:", err);
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
function deregister(dir, pid) {
|
|
5788
|
+
try {
|
|
5789
|
+
const entry = readEntry(dir);
|
|
5790
|
+
if (!entry || entry.pid !== pid) {
|
|
5791
|
+
return;
|
|
5792
|
+
}
|
|
5793
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5794
|
+
writeEntryFile({
|
|
5795
|
+
...entry,
|
|
5796
|
+
pid: null,
|
|
5797
|
+
status: "stopped",
|
|
5798
|
+
stoppedAt: now,
|
|
5799
|
+
updatedAt: now
|
|
5800
|
+
});
|
|
5801
|
+
console.log(` [Registry] Deregistered "${entry.name}"`);
|
|
5802
|
+
} catch (err) {
|
|
5803
|
+
console.warn(" [Registry] Failed to deregister:", err);
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
|
|
5671
5807
|
// src/runtime/server.ts
|
|
5672
|
-
var __dirname =
|
|
5808
|
+
var __dirname = path12.dirname(fileURLToPath2(import.meta.url));
|
|
5673
5809
|
async function startServer(options) {
|
|
5674
5810
|
const {
|
|
5675
5811
|
rootDir,
|
|
5676
5812
|
host = process.env.HOST || "127.0.0.1",
|
|
5677
5813
|
port = Number(process.env.PORT) || 26313,
|
|
5678
|
-
|
|
5814
|
+
daemonRun = false
|
|
5679
5815
|
} = options;
|
|
5680
5816
|
const dataConfig = configManager.load(rootDir);
|
|
5681
5817
|
const apiKey = dataConfig.apiKey || "";
|
|
5682
5818
|
const provider = dataConfig.provider || "openai";
|
|
5819
|
+
const canonicalRootDir = canonicalizeDir(rootDir);
|
|
5820
|
+
const packConfig = loadConfig(canonicalRootDir);
|
|
5683
5821
|
const baseUrl = dataConfig.baseUrl?.trim() || void 0;
|
|
5684
5822
|
const modelId = provider === "anthropic" ? "claude-opus-4-6" : "gpt-5.4";
|
|
5685
|
-
const packageRoot =
|
|
5686
|
-
const webDir =
|
|
5823
|
+
const packageRoot = path12.resolve(__dirname, "..");
|
|
5824
|
+
const webDir = fs13.existsSync(path12.join(rootDir, "web")) ? path12.join(rootDir, "web") : path12.join(packageRoot, "web");
|
|
5687
5825
|
const app = express();
|
|
5688
5826
|
app.use(express.json());
|
|
5689
5827
|
app.use(express.static(webDir));
|
|
5690
5828
|
const server = createServer(app);
|
|
5829
|
+
app.get("/api/health", (_req, res) => {
|
|
5830
|
+
const address = server.address();
|
|
5831
|
+
const actualPort = typeof address === "string" ? port : address?.port ?? port;
|
|
5832
|
+
res.json({
|
|
5833
|
+
status: "ok",
|
|
5834
|
+
dir: canonicalRootDir,
|
|
5835
|
+
name: packConfig.name,
|
|
5836
|
+
version: packConfig.version,
|
|
5837
|
+
port: actualPort,
|
|
5838
|
+
pid: process.pid
|
|
5839
|
+
});
|
|
5840
|
+
});
|
|
5691
5841
|
const lifecycle = new Lifecycle(server);
|
|
5692
5842
|
const agent = new PackAgent({
|
|
5693
5843
|
apiKey,
|
|
@@ -5782,7 +5932,17 @@ async function startServer(options) {
|
|
|
5782
5932
|
Skills Pack Server`);
|
|
5783
5933
|
console.log(` Running at ${url}
|
|
5784
5934
|
`);
|
|
5785
|
-
|
|
5935
|
+
try {
|
|
5936
|
+
register({
|
|
5937
|
+
dir: canonicalRootDir,
|
|
5938
|
+
name: packConfig.name,
|
|
5939
|
+
version: packConfig.version,
|
|
5940
|
+
port: typeof actualPort === "number" ? actualPort : port
|
|
5941
|
+
});
|
|
5942
|
+
} catch (err) {
|
|
5943
|
+
console.warn(" [Registry] Could not register pack:", err);
|
|
5944
|
+
}
|
|
5945
|
+
if (!daemonRun) {
|
|
5786
5946
|
const cmd = process.platform === "darwin" ? `open ${url}` : process.platform === "win32" ? `start ${url}` : `xdg-open ${url}`;
|
|
5787
5947
|
exec(cmd, (err) => {
|
|
5788
5948
|
if (err) console.warn(` Could not open browser: ${err.message}`);
|
|
@@ -5790,9 +5950,11 @@ async function startServer(options) {
|
|
|
5790
5950
|
}
|
|
5791
5951
|
});
|
|
5792
5952
|
process.on("SIGINT", () => {
|
|
5953
|
+
deregister(canonicalRootDir, process.pid);
|
|
5793
5954
|
void lifecycle.requestShutdown("signal");
|
|
5794
5955
|
});
|
|
5795
5956
|
process.on("SIGTERM", () => {
|
|
5957
|
+
deregister(canonicalRootDir, process.pid);
|
|
5796
5958
|
void lifecycle.requestShutdown("signal");
|
|
5797
5959
|
});
|
|
5798
5960
|
await new Promise((resolve, reject) => {
|
|
@@ -5827,23 +5989,23 @@ function findMissingSkills(workDir, config) {
|
|
|
5827
5989
|
});
|
|
5828
5990
|
}
|
|
5829
5991
|
function copyStartTemplates2(workDir) {
|
|
5830
|
-
const templateDir =
|
|
5992
|
+
const templateDir = path13.resolve(
|
|
5831
5993
|
new URL("../templates", import.meta.url).pathname
|
|
5832
5994
|
);
|
|
5833
5995
|
for (const file of ["start.sh", "start.bat"]) {
|
|
5834
|
-
const src =
|
|
5835
|
-
const dest =
|
|
5836
|
-
if (
|
|
5837
|
-
|
|
5996
|
+
const src = path13.join(templateDir, file);
|
|
5997
|
+
const dest = path13.join(workDir, file);
|
|
5998
|
+
if (fs14.existsSync(src)) {
|
|
5999
|
+
fs14.copyFileSync(src, dest);
|
|
5838
6000
|
if (file === "start.sh") {
|
|
5839
|
-
|
|
6001
|
+
fs14.chmodSync(dest, 493);
|
|
5840
6002
|
}
|
|
5841
6003
|
}
|
|
5842
6004
|
}
|
|
5843
6005
|
}
|
|
5844
6006
|
async function runCommand(directory) {
|
|
5845
|
-
const workDir = directory ?
|
|
5846
|
-
|
|
6007
|
+
const workDir = directory ? path13.resolve(directory) : process.cwd();
|
|
6008
|
+
fs14.mkdirSync(workDir, { recursive: true });
|
|
5847
6009
|
if (!configExists(workDir)) {
|
|
5848
6010
|
console.log(chalk4.blue("\n No skillpack.json found. Let's set one up.\n"));
|
|
5849
6011
|
const { name, description } = await inquirer2.prompt([
|
|
@@ -5879,13 +6041,16 @@ async function runCommand(directory) {
|
|
|
5879
6041
|
console.warn(chalk4.yellow(` Warning: Some skills could not be installed: ${err}`));
|
|
5880
6042
|
}
|
|
5881
6043
|
}
|
|
5882
|
-
await startServer({
|
|
6044
|
+
await startServer({
|
|
6045
|
+
rootDir: workDir,
|
|
6046
|
+
daemonRun: process.env.DAEMON_RUN === "1"
|
|
6047
|
+
});
|
|
5883
6048
|
}
|
|
5884
6049
|
|
|
5885
6050
|
// src/cli.ts
|
|
5886
|
-
import
|
|
6051
|
+
import fs15 from "fs";
|
|
5887
6052
|
var packageJson = JSON.parse(
|
|
5888
|
-
|
|
6053
|
+
fs15.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
5889
6054
|
);
|
|
5890
6055
|
var program = new Command();
|
|
5891
6056
|
program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);
|
|
@@ -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
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
|
|