@sellable/install 0.1.308 → 0.1.310
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 +21 -0
- package/bin/sellable-install.mjs +151 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,9 +119,30 @@ It creates:
|
|
|
119
119
|
/srv/hermes/profiles/acme/sellable/config.json
|
|
120
120
|
/srv/hermes/profiles/acme/sellable/configs/
|
|
121
121
|
/srv/hermes/profiles/acme/skills/sellable/
|
|
122
|
+
/srv/hermes/bin/entrypoint-acme.sh
|
|
122
123
|
/srv/hermes/profiles/acme/.env # only when Slack token inputs are supplied
|
|
123
124
|
```
|
|
124
125
|
|
|
126
|
+
For profile paths under a gateway root such as
|
|
127
|
+
`/srv/hermes/profiles/acme`, bootstrap also registers
|
|
128
|
+
`/srv/hermes/profiles/acme/skills/sellable` in the gateway root
|
|
129
|
+
`/srv/hermes/config.yaml` as `skills.external_dirs`. Keep this behavior:
|
|
130
|
+
Hermes Desktop command autocomplete scans the gateway/root skill config, not
|
|
131
|
+
only the selected profile's local skill tree.
|
|
132
|
+
|
|
133
|
+
Bootstrap also writes `/srv/hermes/bin/entrypoint-acme.sh`. Point the Hermes
|
|
134
|
+
Docker service at that script when this profile should be the Desktop/dashboard
|
|
135
|
+
default:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
entrypoint: ["/opt/data/bin/entrypoint-acme.sh"]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The wrapper starts `hermes gateway run` when needed, then intentionally runs
|
|
142
|
+
`hermes -p acme dashboard --isolated`. Without `--isolated`, Hermes OS chat can
|
|
143
|
+
see `/sellable-*` commands while Hermes Desktop autocomplete misses them because
|
|
144
|
+
the dashboard backend is not scoped to the selected profile.
|
|
145
|
+
|
|
125
146
|
If no token is supplied, bootstrap still creates a pending
|
|
126
147
|
`sellable/config.json` so the profile path is concrete from the first MCP
|
|
127
148
|
launch. Finish auth with:
|
package/bin/sellable-install.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
writeFileSync,
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
16
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
17
17
|
import { stdout as output } from "node:process";
|
|
18
18
|
import { createInterface } from "node:readline/promises";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
@@ -3663,6 +3663,87 @@ function hermesSkillsRoot(opts = {}) {
|
|
|
3663
3663
|
return join(hermesHome(opts), "skills", "sellable");
|
|
3664
3664
|
}
|
|
3665
3665
|
|
|
3666
|
+
function hermesGatewayRoot(opts = {}) {
|
|
3667
|
+
const profileRoot = hermesHome(opts);
|
|
3668
|
+
const profilesRoot = dirname(profileRoot);
|
|
3669
|
+
if (basename(profilesRoot) !== "profiles") return null;
|
|
3670
|
+
return dirname(profilesRoot);
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
function hermesGatewayConfigPath(opts = {}) {
|
|
3674
|
+
const root = hermesGatewayRoot(opts);
|
|
3675
|
+
if (!root) return null;
|
|
3676
|
+
return join(root, "config.yaml");
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
function hermesDashboardEntrypointPath(opts = {}, profile = "") {
|
|
3680
|
+
const root = hermesGatewayRoot(opts);
|
|
3681
|
+
if (!root) return null;
|
|
3682
|
+
const safeProfile = String(profile || basenameSafe(hermesHome(opts))).replace(
|
|
3683
|
+
/[^A-Za-z0-9_.-]/g,
|
|
3684
|
+
"_"
|
|
3685
|
+
);
|
|
3686
|
+
return join(root, "bin", `entrypoint-${safeProfile}.sh`);
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
function hermesDashboardCommand(profile) {
|
|
3690
|
+
return `hermes -p ${profile} dashboard --isolated --host 0.0.0.0 --port 4860 --no-open --skip-build`;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
function hermesDashboardEntrypointContent(opts, profile) {
|
|
3694
|
+
const quotedProfile = shellQuote(profile);
|
|
3695
|
+
const quotedProfileRoot = shellQuote(hermesHome(opts));
|
|
3696
|
+
const gatewayRoot = hermesGatewayRoot(opts);
|
|
3697
|
+
const quotedGatewayRoot = shellQuote(gatewayRoot || dirname(hermesHome(opts)));
|
|
3698
|
+
const quotedDashboardLog = shellQuote(join(hermesHome(opts), "logs", "dashboard.log"));
|
|
3699
|
+
return `#!/usr/bin/env bash
|
|
3700
|
+
set -euo pipefail
|
|
3701
|
+
|
|
3702
|
+
# This file is generated by sellable hermes profile bootstrap.
|
|
3703
|
+
# It preserves the Hostinger image startup shape, but launches the dashboard in
|
|
3704
|
+
# an isolated Sellable profile. Without --isolated, Hermes OS chat can see
|
|
3705
|
+
# profile skills while Desktop slash autocomplete stays on the default profile.
|
|
3706
|
+
profile=${quotedProfile}
|
|
3707
|
+
profile_root=${quotedProfileRoot}
|
|
3708
|
+
gateway_root=${quotedGatewayRoot}
|
|
3709
|
+
dashboard_log=${quotedDashboardLog}
|
|
3710
|
+
dashboard_port="\${HERMES_DASHBOARD_PORT:-4860}"
|
|
3711
|
+
run_as=()
|
|
3712
|
+
if command -v gosu >/dev/null 2>&1; then
|
|
3713
|
+
run_as=(gosu hermes)
|
|
3714
|
+
fi
|
|
3715
|
+
|
|
3716
|
+
chown -R hermes:hermes "$gateway_root" 2>/dev/null || true
|
|
3717
|
+
mkdir -p "$gateway_root/logs" "$profile_root/logs"
|
|
3718
|
+
chown -R hermes:hermes "$gateway_root/logs" "$profile_root/logs" 2>/dev/null || true
|
|
3719
|
+
|
|
3720
|
+
if [[ -f "$gateway_root/config.yaml" ]]; then
|
|
3721
|
+
if ! pgrep -f "hermes gateway run" >/dev/null 2>&1; then
|
|
3722
|
+
"\${run_as[@]}" nohup hermes gateway run >>"$gateway_root/logs/gateway.log" 2>&1 </dev/null &
|
|
3723
|
+
fi
|
|
3724
|
+
fi
|
|
3725
|
+
|
|
3726
|
+
for v in ADMIN_USERNAME ADMIN_PASSWORD; do
|
|
3727
|
+
if [[ -z "\${!v:-}" ]]; then
|
|
3728
|
+
echo "missing required environment variable $v" >&2
|
|
3729
|
+
exit 1
|
|
3730
|
+
fi
|
|
3731
|
+
done
|
|
3732
|
+
|
|
3733
|
+
export HERMES_DASHBOARD_BASIC_AUTH_USERNAME="$ADMIN_USERNAME"
|
|
3734
|
+
export HERMES_DASHBOARD_BASIC_AUTH_PASSWORD="$ADMIN_PASSWORD"
|
|
3735
|
+
|
|
3736
|
+
exec "\${run_as[@]}" hermes -p "$profile" dashboard --isolated --host 0.0.0.0 --port "$dashboard_port" --no-open --skip-build >>"$dashboard_log" 2>&1
|
|
3737
|
+
`;
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
function writeHermesDashboardEntrypoint(opts, profile) {
|
|
3741
|
+
const path = hermesDashboardEntrypointPath(opts, profile);
|
|
3742
|
+
if (!path) return null;
|
|
3743
|
+
writeFile(path, hermesDashboardEntrypointContent(opts, profile), opts, 0o755);
|
|
3744
|
+
return path;
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3666
3747
|
function hermesLikelyInstalled() {
|
|
3667
3748
|
return (
|
|
3668
3749
|
Boolean(process.env.HERMES_HOME?.trim()) ||
|
|
@@ -3742,6 +3823,38 @@ function writeHermesMcpServer(opts) {
|
|
|
3742
3823
|
return configPath;
|
|
3743
3824
|
}
|
|
3744
3825
|
|
|
3826
|
+
function writeHermesGatewaySkillDirectory(opts) {
|
|
3827
|
+
const configPath = hermesGatewayConfigPath(opts);
|
|
3828
|
+
if (!configPath || configPath === hermesConfigPath(opts)) return null;
|
|
3829
|
+
|
|
3830
|
+
const skillDir = hermesSkillsRoot(opts);
|
|
3831
|
+
const raw = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
3832
|
+
const config = parseHermesConfig(raw, configPath);
|
|
3833
|
+
// Hermes Desktop slash autocomplete scans the gateway/root HERMES_HOME. A
|
|
3834
|
+
// profile can execute its own skills without this, but Desktop will not show
|
|
3835
|
+
// /sellable-* commands until the root scanner sees the profile skill dir.
|
|
3836
|
+
const skills =
|
|
3837
|
+
config.skills && typeof config.skills === "object" && !Array.isArray(config.skills)
|
|
3838
|
+
? config.skills
|
|
3839
|
+
: {};
|
|
3840
|
+
const current = Array.isArray(skills.external_dirs)
|
|
3841
|
+
? skills.external_dirs
|
|
3842
|
+
: skills.external_dirs
|
|
3843
|
+
? [skills.external_dirs]
|
|
3844
|
+
: [];
|
|
3845
|
+
const externalDirs = current.includes(skillDir) ? current : [...current, skillDir];
|
|
3846
|
+
const nextConfig = {
|
|
3847
|
+
...config,
|
|
3848
|
+
skills: {
|
|
3849
|
+
...skills,
|
|
3850
|
+
external_dirs: externalDirs,
|
|
3851
|
+
},
|
|
3852
|
+
};
|
|
3853
|
+
|
|
3854
|
+
writeHermesConfig(configPath, nextConfig, opts);
|
|
3855
|
+
return { configPath, skillDir };
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3745
3858
|
function installHermesSkills(opts) {
|
|
3746
3859
|
const root = hermesSkillsRoot(opts);
|
|
3747
3860
|
for (const skill of hermesSkillDefinitions()) {
|
|
@@ -3762,8 +3875,15 @@ function installHermes(opts) {
|
|
|
3762
3875
|
try {
|
|
3763
3876
|
skillsRoot = installHermesSkills(opts);
|
|
3764
3877
|
const configPath = writeHermesMcpServer(opts);
|
|
3878
|
+
const gateway = writeHermesGatewaySkillDirectory(opts);
|
|
3765
3879
|
logVerbose(`${C.grey}+ Hermes config updated: ${configPath}${C.reset}`);
|
|
3766
|
-
return {
|
|
3880
|
+
return {
|
|
3881
|
+
installed: true,
|
|
3882
|
+
configPath,
|
|
3883
|
+
skillsRoot,
|
|
3884
|
+
gatewayConfigPath: gateway?.configPath || null,
|
|
3885
|
+
gatewayExternalSkillsDir: gateway?.skillDir || null,
|
|
3886
|
+
};
|
|
3767
3887
|
} catch (err) {
|
|
3768
3888
|
if (skillsRoot && !opts.dryRun) {
|
|
3769
3889
|
rmSync(skillsRoot, { recursive: true, force: true });
|
|
@@ -4001,6 +4121,8 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4001
4121
|
sellableConfigPath,
|
|
4002
4122
|
sellableConfigsDir,
|
|
4003
4123
|
skillsRoot: hermesSkillsRoot(opts),
|
|
4124
|
+
gatewayConfigPath: hermesGatewayConfigPath(opts),
|
|
4125
|
+
hermesDashboardEntrypointPath: hermesDashboardEntrypointPath(opts, profile),
|
|
4004
4126
|
},
|
|
4005
4127
|
auth: {
|
|
4006
4128
|
apiUrl: opts.apiUrl,
|
|
@@ -4020,6 +4142,12 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4020
4142
|
.map(([key, value]) => [key, tokenFingerprint(value)])
|
|
4021
4143
|
),
|
|
4022
4144
|
},
|
|
4145
|
+
dashboard: {
|
|
4146
|
+
command: hermesDashboardCommand(profile),
|
|
4147
|
+
composeEntrypoint: hermesDashboardEntrypointPath(opts, profile)
|
|
4148
|
+
? `entrypoint: ["${hermesDashboardEntrypointPath(opts, profile)}"]`
|
|
4149
|
+
: null,
|
|
4150
|
+
},
|
|
4023
4151
|
created: [],
|
|
4024
4152
|
updated: [],
|
|
4025
4153
|
preserved: [],
|
|
@@ -4085,6 +4213,12 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4085
4213
|
}
|
|
4086
4214
|
|
|
4087
4215
|
const hermesConfigExisted = existsSync(hermesConfigPath(opts));
|
|
4216
|
+
const gatewayConfigPath = hermesGatewayConfigPath(opts);
|
|
4217
|
+
const gatewayConfigExisted = gatewayConfigPath ? existsSync(gatewayConfigPath) : false;
|
|
4218
|
+
const dashboardEntrypointPath = hermesDashboardEntrypointPath(opts, profile);
|
|
4219
|
+
const dashboardEntrypointExisted = dashboardEntrypointPath
|
|
4220
|
+
? existsSync(dashboardEntrypointPath)
|
|
4221
|
+
: false;
|
|
4088
4222
|
const skillsExisted = existsSync(hermesSkillsRoot(opts));
|
|
4089
4223
|
const authExisted = existsSync(sellableConfigPath);
|
|
4090
4224
|
const configsDirExisted = existsSync(sellableConfigsDir);
|
|
@@ -4123,6 +4257,7 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4123
4257
|
mkdirSync(sellableConfigsDir, { recursive: true, mode: 0o700 });
|
|
4124
4258
|
writeHermesProfileSellableConfig(sellableConfigPath, nextAuth, opts);
|
|
4125
4259
|
installHermes(opts);
|
|
4260
|
+
writeHermesDashboardEntrypoint(opts, profile);
|
|
4126
4261
|
if (Object.values(slackTokens).some(Boolean)) {
|
|
4127
4262
|
for (const [key, value] of Object.entries(slackTokens)) {
|
|
4128
4263
|
if (value) envState.entries.set(key, value);
|
|
@@ -4138,6 +4273,20 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4138
4273
|
(hermesConfigExisted ? summary.updated : summary.created).push(
|
|
4139
4274
|
hermesConfigPath(opts)
|
|
4140
4275
|
);
|
|
4276
|
+
if (gatewayConfigPath) {
|
|
4277
|
+
(gatewayConfigExisted ? summary.updated : summary.created).push(
|
|
4278
|
+
gatewayConfigPath
|
|
4279
|
+
);
|
|
4280
|
+
}
|
|
4281
|
+
if (dashboardEntrypointPath) {
|
|
4282
|
+
(dashboardEntrypointExisted ? summary.updated : summary.created).push(
|
|
4283
|
+
dashboardEntrypointPath
|
|
4284
|
+
);
|
|
4285
|
+
} else {
|
|
4286
|
+
summary.skipped.push(
|
|
4287
|
+
"Hermes dashboard entrypoint: profile root is not under a gateway profiles directory"
|
|
4288
|
+
);
|
|
4289
|
+
}
|
|
4141
4290
|
(skillsExisted ? summary.updated : summary.created).push(hermesSkillsRoot(opts));
|
|
4142
4291
|
if (Object.values(slackTokens).some(Boolean)) {
|
|
4143
4292
|
(envExisted ? summary.updated : summary.created).push(envPath);
|