From 02394beee2871ddfa3c144e90f377d967881ab67 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 13 May 2026 08:22:18 +0000 Subject: [PATCH] feat(channels): canonicalise channel name and hint missing preset `channels add` and `channels remove` previously echoed the raw `channelArg` back into log lines, registry writes, and rebuild reasons, so `channels add Telegram` and `channels add telegram` left mismatched strings behind. Lowercase + trim the arg once after `getChannelDef` narrows it, then use the canonical form everywhere downstream. After a successful `channels add`, also print a one-line hint when the matching network preset (telegram/discord/slack) exists in the builtin list but is not yet applied to the sandbox, pointing the user at the `policy-add ` command. Without the preset the rebuilt sandbox cannot reach the channel service, which is currently a silent failure mode. Signed-off-by: Tinson Lai --- src/lib/actions/sandbox/policy-channel.ts | 32 ++++++++++++++++------- test/cli.test.ts | 10 +++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/lib/actions/sandbox/policy-channel.ts b/src/lib/actions/sandbox/policy-channel.ts index c85bd645492..442b45c195a 100644 --- a/src/lib/actions/sandbox/policy-channel.ts +++ b/src/lib/actions/sandbox/policy-channel.ts @@ -410,9 +410,10 @@ export async function addSandboxChannel(sandboxName: string, args: string[] = [] console.error(` Valid channels: ${knownChannelNames().join(", ")}`); process.exit(1); } + const canonical = channelArg.trim().toLowerCase(); if (dryRun) { - console.log(` --dry-run: would enable channel '${channelArg}' for '${sandboxName}'.`); + console.log(` --dry-run: would enable channel '${canonical}' for '${sandboxName}'.`); return; } @@ -428,7 +429,7 @@ export async function addSandboxChannel(sandboxName: string, args: string[] = [] continue; } if (isNonInteractive()) { - console.error(` Missing ${envKey} for channel '${channelArg}'.`); + console.error(` Missing ${envKey} for channel '${canonical}'.`); console.error( ` Set ${envKey} in the environment or via '${CLI_NAME} credentials' before running in non-interactive mode.`, ); @@ -450,9 +451,21 @@ export async function addSandboxChannel(sandboxName: string, args: string[] = [] // discard the change. Pre-fix this was safe because saveCredential() // wrote credentials.json; with env-only persistence, exiting before // the rebuild used to drop the queued token. - await applyChannelAddToGatewayAndRegistry(sandboxName, channelArg, acquired); - console.log(` ${G}✓${R} Registered ${channelArg} bridge with the OpenShell gateway.`); - await promptAndRebuild(sandboxName, `add '${channelArg}'`); + await applyChannelAddToGatewayAndRegistry(sandboxName, canonical, acquired); + console.log(` ${G}✓${R} Registered ${canonical} bridge with the OpenShell gateway.`); + maybeHintPolicyPresetForChannel(sandboxName, canonical); + await promptAndRebuild(sandboxName, `add '${canonical}'`); +} + +function maybeHintPolicyPresetForChannel(sandboxName: string, channelName: string): void { + const presetExists = policies.listPresets().some((p) => p.name === channelName); + if (!presetExists) return; + const applied = policies.getAppliedPresets(sandboxName); + if (applied.includes(channelName)) return; + console.log( + ` Hint: the ${channelName} network preset is not applied to '${sandboxName}'. ` + + `Run \`${CLI_NAME} ${sandboxName} policy-add ${channelName}\` so the rebuilt sandbox can reach the ${channelName} service.`, + ); } export async function removeSandboxChannel(sandboxName: string, args: string[] = []): Promise { @@ -470,9 +483,10 @@ export async function removeSandboxChannel(sandboxName: string, args: string[] = console.error(` Valid channels: ${knownChannelNames().join(", ")}`); process.exit(1); } + const canonical = channelArg.trim().toLowerCase(); if (dryRun) { - console.log(` --dry-run: would remove channel '${channelArg}' for '${sandboxName}'.`); + console.log(` --dry-run: would remove channel '${canonical}' for '${sandboxName}'.`); return; } @@ -483,11 +497,11 @@ export async function removeSandboxChannel(sandboxName: string, args: string[] = // already "removed" from the user's perspective. await applyChannelRemoveToGatewayAndRegistry( sandboxName, - channelArg, + canonical, getChannelTokenKeys(channel), ); - console.log(` ${G}✓${R} Removed ${channelArg} bridge from the OpenShell gateway.`); - await promptAndRebuild(sandboxName, `remove '${channelArg}'`); + console.log(` ${G}✓${R} Removed ${canonical} bridge from the OpenShell gateway.`); + await promptAndRebuild(sandboxName, `remove '${canonical}'`); } async function sandboxChannelsSetEnabled( diff --git a/test/cli.test.ts b/test/cli.test.ts index bd910ccec8a..016731d8202 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1700,10 +1700,20 @@ describe("CLI dispatch", () => { expect(add.code).toBe(0); expect(add.out).toContain("--dry-run: would enable channel 'telegram' for 'alpha'."); + const addMixedCase = runWithEnv("alpha channels add Telegram --dry-run", { HOME: home }); + expect(addMixedCase.code).toBe(0); + expect(addMixedCase.out).toContain("--dry-run: would enable channel 'telegram' for 'alpha'."); + const remove = runWithEnv("alpha channels remove telegram --dry-run", { HOME: home }); expect(remove.code).toBe(0); expect(remove.out).toContain("--dry-run: would remove channel 'telegram' for 'alpha'."); + const removeMixedCase = runWithEnv("alpha channels remove Telegram --dry-run", { HOME: home }); + expect(removeMixedCase.code).toBe(0); + expect(removeMixedCase.out).toContain( + "--dry-run: would remove channel 'telegram' for 'alpha'.", + ); + const stop = runWithEnv("alpha channels stop telegram --dry-run", { HOME: home }); expect(stop.code).toBe(0); expect(stop.out).toContain("--dry-run: would stop channel 'telegram' for 'alpha'.");