windsor-bot 0.1.11 → 0.1.13

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/server.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { createServer } from 'http';
2
+ import { execFile } from 'child_process';
2
3
  import { build } from 'esbuild';
3
4
  import { readFile, stat } from 'fs/promises';
5
+ import { promisify } from 'util';
6
+ import { spawn } from 'child_process';
4
7
  import { dirname, join } from 'path';
5
8
  import { fileURLToPath } from 'url';
6
9
  import { getCurrentConfig, updateConfig, checkPassword, hashPassword, getConfigFilePath, } from './config.js';
7
10
  import { printTestPage, getImageFeed, clearImageFeed, renderFeedHtml } from './printing/index.js';
8
11
  const MAX_EVENTS = 200;
9
12
  const events = [];
13
+ const execFileAsync = promisify(execFile);
10
14
  export const status = {
11
15
  connected: false,
12
16
  tag: null,
@@ -53,6 +57,38 @@ function sendJson(res, statusCode, payload) {
53
57
  });
54
58
  res.end(body);
55
59
  }
60
+ async function readPackageVersion() {
61
+ const runtimeDir = dirname(fileURLToPath(import.meta.url));
62
+ const packageJson = JSON.parse(await readFile(join(runtimeDir, '..', 'package.json'), 'utf8'));
63
+ if (typeof packageJson.version !== 'string')
64
+ throw new Error('package.json does not contain a valid version');
65
+ return packageJson.version;
66
+ }
67
+ async function updateGlobalBot() {
68
+ await execFileAsync('npm', ['install', '-g', 'windsor-bot@latest']);
69
+ const { stdout } = await execFileAsync('npm', ['root', '-g']);
70
+ const packageJsonPath = join(stdout.trim(), 'windsor-bot', 'package.json');
71
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
72
+ if (typeof packageJson.version !== 'string')
73
+ throw new Error('Updated package does not contain a valid version');
74
+ return packageJson.version;
75
+ }
76
+ function hardRestart() {
77
+ const child = spawn('/bin/sh', [
78
+ '-c',
79
+ 'sleep 1; exec "$@"',
80
+ 'windsor-hard-restart',
81
+ process.execPath,
82
+ ...process.argv.slice(1),
83
+ ], {
84
+ detached: true,
85
+ stdio: 'inherit',
86
+ cwd: process.cwd(),
87
+ env: process.env,
88
+ });
89
+ child.unref();
90
+ setTimeout(() => process.kill(process.pid, 'SIGTERM'), 250);
91
+ }
56
92
  async function readJsonBody(req) {
57
93
  const chunks = [];
58
94
  let total = 0;
@@ -185,6 +221,10 @@ export function startDiagnosticsServer(port) {
185
221
  sendJson(res, 200, status);
186
222
  return;
187
223
  }
224
+ if (method === 'GET' && urlPath === '/api/version') {
225
+ sendJson(res, 200, { version: await readPackageVersion() });
226
+ return;
227
+ }
188
228
  if (method === 'GET' && urlPath === '/api/config') {
189
229
  const cfg = getCurrentConfig();
190
230
  sendJson(res, 200, {
@@ -300,6 +340,18 @@ export function startDiagnosticsServer(port) {
300
340
  }
301
341
  return;
302
342
  }
343
+ if (method === 'POST' && urlPath === '/api/update') {
344
+ const version = await updateGlobalBot();
345
+ logEvent('info', `Updated windsor-bot to version ${version}`);
346
+ sendJson(res, 200, { version });
347
+ return;
348
+ }
349
+ if (method === 'POST' && urlPath === '/api/hard-restart') {
350
+ logEvent('info', 'Hard restart requested via API');
351
+ sendJson(res, 200, { ok: true });
352
+ hardRestart();
353
+ return;
354
+ }
303
355
  if (method === 'POST' && urlPath === '/api/channels/refresh') {
304
356
  if (_refreshChannelsHandler) {
305
357
  await _refreshChannelsHandler();
@@ -1,6 +1,13 @@
1
+ import { mkdtemp, rm } from 'fs/promises';
2
+ import { tmpdir } from 'os';
1
3
  import { test } from 'node:test';
2
4
  import assert from 'node:assert/strict';
3
- import { reconcileChannels, getCurrentConfig, updateConfig, hashPassword } from '../config.js';
5
+ const configDir = await mkdtemp(`${tmpdir()}/windsor-config-test-`);
6
+ process.env['WINDSOR_CONFIG_PATH'] = `${configDir}/windsor.config.json`;
7
+ const { reconcileChannels, getCurrentConfig, updateConfig, hashPassword } = await import('../config.js');
8
+ test.after(async () => {
9
+ await rm(configDir, { recursive: true, force: true });
10
+ });
4
11
  // Setup: initialize config with some channel mappings
5
12
  async function withChannels(mappings, fn) {
6
13
  const original = getCurrentConfig().channels;
@@ -0,0 +1,12 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { dealPokerHand } from '../commands/poker.js';
4
+ test('deals five board cards and four two-card hands without duplicates', () => {
5
+ const deal = dealPokerHand(() => 0.5);
6
+ const cards = [...deal.board, ...deal.hands.flat()];
7
+ const keys = cards.map(card => `${card.rank}${card.suit}`);
8
+ assert.equal(deal.board.length, 5);
9
+ assert.equal(deal.hands.length, 4);
10
+ assert.equal(cards.length, 13);
11
+ assert.equal(new Set(keys).size, cards.length);
12
+ });
@@ -1,6 +1,16 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { extractUrls, stripUrls, replaceUrlsInText } from '../bot.js';
3
+ import { extractUrls, parseCommand, stripUrls, replaceUrlsInText } from '../bot.js';
4
+ test('parses commands with or without a space after the prefix', () => {
5
+ assert.deepEqual(parseCommand('!command'), { name: 'command', args: '' });
6
+ assert.deepEqual(parseCommand('! command'), { name: 'command', args: '' });
7
+ });
8
+ test('parses command arguments after optional prefix whitespace', () => {
9
+ assert.deepEqual(parseCommand('! command first second'), {
10
+ name: 'command',
11
+ args: 'first second',
12
+ });
13
+ });
4
14
  test('extracts a single URL', () => {
5
15
  const urls = extractUrls('Check this out https://example.com for more info');
6
16
  assert.deepEqual(urls, ['https://example.com']);