@anytowork/cli 0.12.27 → 0.12.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anytowork/cli",
3
- "version": "0.12.27",
3
+ "version": "0.12.29",
4
4
  "description": "AnytoWork 平台命令行工具——让任何能执行 shell 命令的 Agent 用一条命令接入 AnytoWork:登录、发单接单、管理能力。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,7 +41,7 @@ export async function runListenInstall(p) {
41
41
  } else {
42
42
  try {
43
43
  result = await (deps.installAutostart || installAutostart)({
44
- profile, configDir: env.ANYTOWORK_CONFIG_DIR || null, logFile: `${dir}/listener.log`,
44
+ profile, configDir: profile ? null : (env.ANYTOWORK_CONFIG_DIR || env.LINGER_CONFIG_DIR || null), logFile: `${dir}/listener.log`,
45
45
  platform: deps.platform, home: deps.home, runCmd: deps.runCmd, node: deps.node, entry: deps.entry, pathEnv: deps.pathEnv,
46
46
  startNow: deps.startNow,
47
47
  });
@@ -64,7 +64,7 @@ export async function runListenUninstall(p) {
64
64
  const { flags, credOpts, baseUrl, fetchImpl, log, deps = {} } = p;
65
65
  const env = deps.env || process.env;
66
66
  try {
67
- await (deps.uninstallAutostart || uninstallAutostart)({ profile: flags.profile || null, platform: deps.platform, home: deps.home, runCmd: deps.runCmd });
67
+ await (deps.uninstallAutostart || uninstallAutostart)({ profile: flags.profile || null, configDir: flags.profile ? null : (env.ANYTOWORK_CONFIG_DIR || env.LINGER_CONFIG_DIR || null), platform: deps.platform, home: deps.home, runCmd: deps.runCmd });
68
68
  } catch { /* nothing to undo */ }
69
69
  for (const f of [configPath(credOpts), cursorFile(credOpts), statePath(credOpts)]) { try { fs.unlinkSync(f); } catch { /* none */ } }
70
70
  try {
@@ -8,9 +8,16 @@
8
8
  import fs from 'node:fs';
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
+ import { createHash } from 'node:crypto';
11
12
  import { execFile } from 'node:child_process';
12
13
  import { fileURLToPath } from 'node:url';
13
14
 
15
+ /** Name part that tells identities apart on one computer: the profile, else a short hash of the config dir, else "default". */
16
+ export function slotOf(profile, configDir) {
17
+ if (profile) return profile;
18
+ if (configDir) return `dir-${createHash("sha1").update(String(configDir)).digest("hex").slice(0, 8)}`;
19
+ return null;
20
+ }
14
21
  export function labelOf(profile) { return `work.anytowork.listen.${profile || 'default'}`; }
15
22
  export function unitNameOf(profile) { return `anytowork-listen-${profile || 'default'}.service`; }
16
23
  export function taskNameOf(profile) { return `AnytoWork Listen ${profile || 'default'}`; }
@@ -79,13 +86,15 @@ function shSingle(s) { return `'${String(s).replace(/'/g, `'\\''`)}'`; }
79
86
  export function cronLine({ node, entry, profile, configDir }) {
80
87
  const cmd = [shSingle(node), shSingle(entry), ...runArgs(profile).map(shSingle)].join(' ');
81
88
  const envPart = configDir ? `ANYTOWORK_CONFIG_DIR=${shSingle(configDir)} ` : '';
82
- return `@reboot ${envPart}${cmd} >/dev/null 2>&1 ${cronMarkOf(profile)}`;
89
+ return `@reboot ${envPart}${cmd} >/dev/null 2>&1 ${cronMarkOf(slotOf(profile, configDir))}`;
83
90
  }
84
91
 
85
92
  /** Windows: the arguments of `schtasks /Create` (run at the user's logon, no administrator needed). */
86
- export function schtasksCreateArgs({ node, entry, profile }) {
87
- const tr = [`"${node}"`, `"${entry}"`, ...runArgs(profile)].join(' ');
88
- return ['/Create', '/TN', taskNameOf(profile), '/SC', 'ONLOGON', '/TR', tr, '/F'];
93
+ export function schtasksCreateArgs({ node, entry, profile, configDir }) {
94
+ const run = [`"${node}"`, `"${entry}"`, ...runArgs(profile)].join(" ");
95
+ // a task cannot carry environment variables, so a custom config dir is set by a small cmd wrapper
96
+ const tr = configDir ? `cmd /c "set ANYTOWORK_CONFIG_DIR=${configDir}&& ${run}"` : run;
97
+ return ["/Create", "/TN", taskNameOf(slotOf(profile, configDir)), "/SC", "ONLOGON", "/TR", tr, "/F"];
89
98
  }
90
99
 
91
100
  /** Default way to run a system command; resolves {code, stdout, stderr}, never rejects. */
@@ -117,11 +126,12 @@ export async function installAutostart(o) {
117
126
  const node = o.node || process.execPath;
118
127
  const entry = o.entry || defaultEntryPath();
119
128
  const pathEnv = o.pathEnv != null ? o.pathEnv : process.env.PATH;
129
+ const slot = slotOf(o.profile, o.configDir);
120
130
  const common = { node, entry, profile: o.profile || null, pathEnv, configDir: o.configDir || null };
121
131
  const notes = [];
122
132
 
123
133
  if (platform === 'darwin') {
124
- const label = labelOf(o.profile);
134
+ const label = labelOf(slot);
125
135
  const dir = path.join(home, 'Library', 'LaunchAgents');
126
136
  const file = path.join(dir, `${label}.plist`);
127
137
  fs.mkdirSync(dir, { recursive: true });
@@ -136,7 +146,7 @@ export async function installAutostart(o) {
136
146
  if (platform === 'win32') {
137
147
  const create = await runCmd('schtasks', schtasksCreateArgs(common));
138
148
  if (create.code !== 0) throw new Error(`schtasks: ${(create.stderr || create.stdout || '').trim().slice(0, 200)}`);
139
- await runCmd('schtasks', ['/Run', '/TN', taskNameOf(o.profile)]);
149
+ await runCmd('schtasks', ['/Run', '/TN', taskNameOf(slot)]);
140
150
  return { kind: 'schtasks', notes };
141
151
  }
142
152
 
@@ -144,17 +154,17 @@ export async function installAutostart(o) {
144
154
  const sd = await runCmd('systemctl', ['--user', '--version']);
145
155
  if (sd.code === 0 && !sd.missing) {
146
156
  const dir = path.join(home, '.config', 'systemd', 'user');
147
- const file = path.join(dir, unitNameOf(o.profile));
157
+ const file = path.join(dir, unitNameOf(slot));
148
158
  fs.mkdirSync(dir, { recursive: true });
149
159
  fs.writeFileSync(file, systemdUnit(common), { mode: 0o644 });
150
160
  await runCmd('systemctl', ['--user', 'daemon-reload']);
151
- const r = await runCmd('systemctl', ['--user', 'enable', '--now', unitNameOf(o.profile)]);
161
+ const r = await runCmd('systemctl', ['--user', 'enable', '--now', unitNameOf(slot)]);
152
162
  if (r.code === 0) { notes.push('linger'); return { kind: 'systemd', file, notes }; }
153
163
  // The user manager is not reachable (container, ssh without a session): fall through to cron.
154
164
  }
155
165
  const ct = await runCmd('crontab', ['-l']);
156
166
  if (!ct.missing) {
157
- const mark = cronMarkOf(o.profile);
167
+ const mark = cronMarkOf(slot);
158
168
  const kept = (ct.code === 0 ? ct.stdout : '').split('\n').filter((l) => l && !l.includes(mark));
159
169
  kept.push(cronLine(common));
160
170
  const w = await runCmd('crontab', ['-'], { input: `${kept.join('\n')}\n` });
@@ -168,30 +178,31 @@ export async function installAutostart(o) {
168
178
 
169
179
  /** Undo installAutostart; unknown or missing pieces are skipped. */
170
180
  export async function uninstallAutostart(o) {
181
+ const slot = slotOf(o.profile, o.configDir);
171
182
  const platform = o.platform || process.platform;
172
183
  const runCmd = o.runCmd || defaultRunCmd;
173
184
  const home = o.home || os.homedir();
174
185
  if (platform === 'darwin') {
175
- const label = labelOf(o.profile);
186
+ const label = labelOf(slot);
176
187
  const uid = String(o.uid != null ? o.uid : (process.getuid ? process.getuid() : 501));
177
188
  await runCmd('launchctl', ['bootout', `gui/${uid}/${label}`]);
178
189
  try { fs.unlinkSync(path.join(home, 'Library', 'LaunchAgents', `${label}.plist`)); } catch { /* none */ }
179
190
  return;
180
191
  }
181
192
  if (platform === 'win32') {
182
- await runCmd('schtasks', ['/End', '/TN', taskNameOf(o.profile)]);
183
- await runCmd('schtasks', ['/Delete', '/TN', taskNameOf(o.profile), '/F']);
193
+ await runCmd('schtasks', ['/End', '/TN', taskNameOf(slot)]);
194
+ await runCmd('schtasks', ['/Delete', '/TN', taskNameOf(slot), '/F']);
184
195
  return;
185
196
  }
186
- const unit = path.join(home, '.config', 'systemd', 'user', unitNameOf(o.profile));
197
+ const unit = path.join(home, '.config', 'systemd', 'user', unitNameOf(slot));
187
198
  if (fs.existsSync(unit)) {
188
- await runCmd('systemctl', ['--user', 'disable', '--now', unitNameOf(o.profile)]);
199
+ await runCmd('systemctl', ['--user', 'disable', '--now', unitNameOf(slot)]);
189
200
  try { fs.unlinkSync(unit); } catch { /* none */ }
190
201
  await runCmd('systemctl', ['--user', 'daemon-reload']);
191
202
  }
192
203
  const ct = await runCmd('crontab', ['-l']);
193
204
  if (!ct.missing && ct.code === 0) {
194
- const mark = cronMarkOf(o.profile);
205
+ const mark = cronMarkOf(slot);
195
206
  if (ct.stdout.includes(mark)) {
196
207
  const kept = ct.stdout.split('\n').filter((l) => l && !l.includes(mark));
197
208
  await runCmd('crontab', ['-'], { input: kept.length ? `${kept.join('\n')}\n` : '' });
@@ -21,9 +21,11 @@ function langOf(payload, env) {
21
21
 
22
22
  /**
23
23
  * @param {object} payload the `wb.wake` payload ({group, session, from, summary, rules, boundary, rewake, ...})
24
+ * @param {{profile?: string, dir?: string}|null} identity set when this Agent is not the default identity of the computer:
25
+ * an Agent that runs inside another process (a gateway) does not inherit our environment, so the prompt names it
24
26
  * @returns {string}
25
27
  */
26
- export function buildPrompt(payload, env = process.env) {
28
+ export function buildPrompt(payload, env = process.env, identity = null) {
27
29
  const p = payload || {};
28
30
  const lang = langOf(p, env);
29
31
  const envLang = { ...env, ANYTOWORK_LANG: lang };
@@ -57,6 +59,14 @@ export function buildPrompt(payload, env = process.env) {
57
59
  ? `Session id ${sid}. This is an automatic call from the platform.`
58
60
  : `会话编号 ${sid}。这是平台自动叫醒。`);
59
61
  if (p.rewake === true) lines.push(REWAKE_LINE[lang]);
62
+ if (identity && (identity.profile || identity.dir)) {
63
+ const pf = identity.profile;
64
+ lines.push(lang === 'en'
65
+ ? (pf ? `Your identity on this computer is "${pf}": put --profile ${pf} right after anytowork in every command (for example: anytowork --profile ${pf} chat read ${sid}), or the wrong identity is used.`
66
+ : `Your identity on this computer lives in ${identity.dir}: set ANYTOWORK_CONFIG_DIR=${identity.dir} for every anytowork command, or the wrong identity is used.`)
67
+ : (pf ? `你在这台电脑上的身份是「${pf}」:每条 anytowork 命令都要在 anytowork 后面加 --profile ${pf}(例如 anytowork --profile ${pf} chat read ${sid}),否则会用错身份。`
68
+ : `你在这台电脑上的身份放在 ${identity.dir}:每条 anytowork 命令都要设置环境变量 ANYTOWORK_CONFIG_DIR=${identity.dir},否则会用错身份。`));
69
+ }
60
70
  return lines.join('\n');
61
71
  }
62
72
 
@@ -141,6 +141,14 @@ export class Listener {
141
141
  this.credDir = resolveCredDir(this.credOpts);
142
142
  }
143
143
 
144
+ /** Who this listener is when it is not the computer's default identity (named in the prompt, see buildPrompt). */
145
+ identity() {
146
+ const profile = this.credOpts && this.credOpts.profile ? this.credOpts.profile : null;
147
+ if (profile) return { profile };
148
+ const named = String(this.env.ANYTOWORK_CONFIG_DIR || this.env.LINGER_CONFIG_DIR || '').trim();
149
+ return named ? { dir: this.credDir } : null;
150
+ }
151
+
144
152
  // ---- state helpers ---------------------------------------------------------------------------------
145
153
  save() { writeState(this.state, this.credOpts); }
146
154
  logLine(line) { appendLog(this.credOpts, line, this.nowMs()); }
@@ -243,7 +251,7 @@ export class Listener {
243
251
  } catch { /* cannot tell: run it */ }
244
252
  }
245
253
  const promptFile = path.join(promptsDir(this.credOpts), `${job.wakeId || job.sid.slice(0, 8)}-${now0}.txt`);
246
- const prompt = buildPrompt(job.payload, this.env);
254
+ const prompt = buildPrompt(job.payload, this.env, this.identity());
247
255
  try {
248
256
  fs.mkdirSync(path.dirname(promptFile), { recursive: true, mode: 0o700 });
249
257
  fs.writeFileSync(promptFile, prompt, { mode: 0o600 });
@@ -339,6 +347,7 @@ export class Listener {
339
347
  reconnectMs = RECONNECT_FIRST_MS;
340
348
  if (data && data.next_cursor != null) { cursor = data.next_cursor; writeListenerCursor(cursor, this.credOpts); }
341
349
  this.state.seenAt = this.nowMs();
350
+ if (this.state.seenAt - (this.lastSeenSave || 0) >= 30000) { this.lastSeenSave = this.state.seenAt; this.save(); } // lets `listen status` tell it is alive
342
351
  const lis = data && data.listener;
343
352
  if (lis && lis.allowed === false) {
344
353
  // The platform asks to stop waking for now (for example the wake-ups are not being read). Check back later.