herdrchat 0.1.0 → 1.1.0

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
@@ -7,6 +7,7 @@ Every terminal hc runs, in the chat app you already use. Feishu first.
7
7
  [English](README.md) · [简体中文](README.zh.md)
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/herdrchat.svg)](https://www.npmjs.com/package/herdrchat)
10
+ [![CI](https://github.com/zhangxiangliang/herdrchat/actions/workflows/ci.yml/badge.svg)](https://github.com/zhangxiangliang/herdrchat/actions/workflows/ci.yml)
10
11
  [![license](https://img.shields.io/npm/l/herdrchat.svg)](https://github.com/zhangxiangliang/herdrchat/blob/main/LICENSE)
11
12
 
12
13
  </div>
package/README.zh.md CHANGED
@@ -7,6 +7,7 @@ hc 跑的每个终端,都能在你常用的聊天软件里看到和回复。
7
7
  [English](README.md) · [简体中文](README.zh.md)
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/herdrchat.svg)](https://www.npmjs.com/package/herdrchat)
10
+ [![CI](https://github.com/zhangxiangliang/herdrchat/actions/workflows/ci.yml/badge.svg)](https://github.com/zhangxiangliang/herdrchat/actions/workflows/ci.yml)
10
11
  [![license](https://img.shields.io/npm/l/herdrchat.svg)](https://github.com/zhangxiangliang/herdrchat/blob/main/LICENSE)
11
12
 
12
13
  </div>
package/bin/cli.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  // run run in the foreground (what start keeps running)
11
11
  //
12
12
  // The profile is --profile or HERDRCHAT_PROFILE (default "default"); where its files are
13
- // is in lib/config.mjs.
13
+ // is in lib/core/config.mjs.
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
@@ -39,13 +39,52 @@ const lines = Number(flag('-n') || 40);
39
39
  const errorsOnly = argv.includes('--errors');
40
40
  const command = argv.find((a) => !a.startsWith('-'));
41
41
 
42
- // after the profile is set: lib/config.mjs reads it when it loads
43
- const { CONFIG_FILE, STATE_DIR, loadConfig, makeDirs, placesOf } = await import('../lib/config.mjs');
44
- const { hc } = await import('../lib/hc.mjs');
42
+ // after the profile is set: lib/core/config.mjs reads it when it loads
43
+ const { CONFIG_FILE, STATE_DIR, loadConfig, makeDirs, placesOf } = await import('../lib/core/config.mjs');
44
+ const { hc } = await import('../lib/core/hc.mjs');
45
45
  const places = placesOf();
46
46
 
47
47
  const shellQuote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
48
48
  const firstSession = () => loadConfig().sessions[0];
49
+ const CONNECT_WAIT_MS = 30000;
50
+
51
+ // hc's full path, from HERDRCHAT_HC or PATH
52
+ function findHc() {
53
+ const dirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
54
+ for (const file of [process.env.HERDRCHAT_HC, ...dirs.map((d) => path.join(d, 'hc'))].filter(Boolean)) {
55
+ try {
56
+ fs.accessSync(file, fs.constants.X_OK);
57
+ return file;
58
+ } catch {
59
+ // not there
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+
65
+ // The bot runs in the first session listed; after that list changes, an older one may
66
+ // still run in another, and two bots answer every message: it is stopped everywhere.
67
+ async function stopEverywhere() {
68
+ for (const session of loadConfig().sessions) {
69
+ await hc(session, ['kill', places.terminal]).catch(() => {});
70
+ await hc(session, ['rm', places.terminal]).catch(() => {});
71
+ }
72
+ }
73
+
74
+ // Started is not running: wait until today's log says the bot reached Feishu after
75
+ // `since`, and hand back the errors it wrote meanwhile when it does not.
76
+ async function waitConnected(since) {
77
+ const log = path.join(STATE_DIR, 'logs', `${new Date().toISOString().slice(0, 10)}.log`);
78
+ const until = Date.now() + CONNECT_WAIT_MS;
79
+ let errors = [];
80
+ while (Date.now() < until) {
81
+ await new Promise((r) => setTimeout(r, 500));
82
+ const lines = fs.existsSync(log) ? fs.readFileSync(log, 'utf8').split('\n').filter((l) => l.slice(0, 24) >= since) : [];
83
+ if (lines.some((l) => /\[\w+\] connected\b/.test(l))) return { ok: true }; // the chat app says it is in
84
+ errors = lines.filter((l) => / ERROR /.test(l));
85
+ }
86
+ return { ok: false, errors: [...new Set(errors.map((l) => l.slice(25)))].slice(-5) };
87
+ }
49
88
 
50
89
  async function terminalState(session) {
51
90
  try {
@@ -69,24 +108,34 @@ const commands = {
69
108
 
70
109
  async start() {
71
110
  const session = firstSession();
72
- // hc starts a program with its own environment: the profile and folders go along
73
- const env = ['HERDRCHAT_PROFILE', 'HERDRCHAT_HOME', 'XDG_CONFIG_HOME', 'XDG_STATE_HOME']
74
- .filter((k) => process.env[k])
75
- .map((k) => `${k}=${shellQuote(process.env[k])}`)
111
+ const hcPath = findHc();
112
+ if (!hcPath) throw new Error('hc is not on PATH: install herdr-core first (https://herdr.app)');
113
+ // hc starts a program with the core's environment, not ours, and a core started by a
114
+ // service may not have hc on its PATH: hc's full path, our PATH, the profile and the
115
+ // folders go along
116
+ const env = [['HERDRCHAT_HC', hcPath], ...['PATH', 'HERDRCHAT_PROFILE', 'HERDRCHAT_HOME', 'XDG_CONFIG_HOME', 'XDG_STATE_HOME'].map((k) => [k, process.env[k]])]
117
+ .filter(([, v]) => v)
118
+ .map(([k, v]) => `${k}=${shellQuote(v)}`)
76
119
  .join(' ');
77
120
  const run = `env ${env} ${shellQuote(process.execPath)} ${shellQuote(path.join(ROOT, 'bin', 'herdrchat.mjs'))}`;
78
121
  const loop = `while true; do ${run}; echo 'herdrchat exited, restarting in 3 s'; sleep 3; done`;
79
- await hc(session, ['kill', places.terminal]).catch(() => {});
80
- await hc(session, ['rm', places.terminal]).catch(() => {});
122
+ await stopEverywhere();
123
+ const since = new Date().toISOString();
81
124
  await hc(session, ['run', '--bg', '--no-parent', '--name', places.terminal, '--cwd', ROOT, '--', 'sh', '-c', loop]);
82
- console.log(`Started ${places.terminal} (profile ${places.profile}) in hc session ${session}.`);
125
+ const result = await waitConnected(since);
126
+ if (!result.ok) {
127
+ console.error(`${places.terminal} started in hc session ${session} but did not reach its chat app within ${CONNECT_WAIT_MS / 1000} s.`);
128
+ if (result.errors.length) console.error(`Its errors:\n ${result.errors.join('\n ')}`);
129
+ console.error('It keeps retrying; `herdrchat logs --errors` shows more.');
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+ console.log(`Started ${places.terminal} (profile ${places.profile}) in hc session ${session}; connected to its chat app.`);
83
134
  },
84
135
 
85
136
  async stop() {
86
- const session = firstSession();
87
- await hc(session, ['kill', places.terminal]).catch(() => {});
88
- await hc(session, ['rm', places.terminal]).catch(() => {});
89
- console.log(`Stopped ${places.terminal}.`);
137
+ await stopEverywhere();
138
+ console.log(`Stopped ${places.terminal}. Until it starts again, nothing reaches Feishu.`);
90
139
  },
91
140
 
92
141
  async status() {