@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 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:
@@ -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 { installed: true, configPath, skillsRoot };
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.308",
3
+ "version": "0.1.310",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {