Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/core/plugin/lifecycle-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,12 @@ export class LifecycleManager {
this.contexts.set(plugin.name, ctx)
this._loaded.add(plugin.name)
this.eventBus?.emit(BusEvent.PLUGIN_LOADED, { name: plugin.name, version: plugin.version })

// Special handling for manually installed channel adapters (helps Windows users)
if (['@openacp/slack-adapter', '@openacp/discord-adapter'].includes(plugin.name)) {
const channelId = plugin.name.includes('slack') ? 'slack' : 'discord'
this.eventBus?.emit('channel:adapter:registered', { channelId, pluginName: plugin.name })
}
} catch (err) {
this._failed.add(plugin.name)
ctx.cleanup()
Expand Down
27 changes: 21 additions & 6 deletions src/core/plugin/plugin-installer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { spawn } from 'node:child_process'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { pathToFileURL } from 'node:url'

const execFileAsync = promisify(execFile)

/**
* Import a package resolved from a specific directory (not the project root).
*
Expand Down Expand Up @@ -70,8 +67,26 @@ export async function installNpmPlugin(packageName: string, pluginsDir?: string)
// Not installed, proceed with install
}

await execFileAsync('npm', ['install', packageName, '--prefix', dir, '--save', '--ignore-scripts'], {
timeout: 60000,
// More robust npm install (especially on Windows)
await new Promise<void>((resolve, reject) => {
const child = spawn('npm', [
'install', packageName,
'--prefix', dir,
'--save',
'--ignore-scripts',
'--no-audit',
'--no-fund'
], {
stdio: 'inherit',
shell: process.platform === 'win32',
windowsHide: true,
})

child.on('close', (code) => {
if (code === 0) resolve()
else reject(new Error(`npm install failed with exit code ${code}`))
})
child.on('error', reject)
})

return await importFromDir(packageName, dir)
Expand Down
8 changes: 8 additions & 0 deletions src/core/setup/setup-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { guardCancel, ok, c } from "./helpers.js";
// Telegram is built-in, so it uses a direct import path instead of this map.
const CHANNEL_PLUGIN_NAME: Record<string, string> = {
discord: "@openacp/discord-adapter",
slack: "@openacp/slack-adapter",
};

/**
Expand Down Expand Up @@ -43,6 +44,13 @@ export async function getChannelStatuses(config: Config, settingsManager?: Setti
enabled = ps.enabled !== false;
hint = ps.guildId ? `Guild: ${ps.guildId}` : undefined;
}
} else if (settingsManager && id === "slack") {
const ps = await settingsManager.loadSettings("@openacp/slack-adapter");
if (ps.botToken && ps.appToken) {
configured = true;
enabled = ps.enabled !== false;
hint = "Connected";
}
}

statuses.push({ id, label: meta.label, configured, enabled, hint });
Expand Down
7 changes: 4 additions & 3 deletions src/core/setup/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ export const ONBOARD_SECTION_OPTIONS: Array<{

/** Display metadata for each core-managed channel type. */
export const CHANNEL_META: Record<string, { label: string; method: string }> = {
sse: { label: "Desktop App", method: "SSE" },
telegram: { label: "Telegram", method: "Bot API" },
discord: { label: "Discord", method: "Bot API" },
sse: { label: "Desktop App", method: "SSE" },
telegram:{ label: "Telegram", method: "Bot API" },
discord: { label: "Discord", method: "Bot API" },
slack: { label: "Slack", method: "Bot API" },
};

/** A community adapter discovered from the OpenACP plugin registry. */
Expand Down