@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 +1 -1
- package/src/listener/commands.js +2 -2
- package/src/listener/install.js +26 -15
- package/src/listener/prompt.js +11 -1
- package/src/listener/run.js +10 -1
package/package.json
CHANGED
package/src/listener/commands.js
CHANGED
|
@@ -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 {
|
package/src/listener/install.js
CHANGED
|
@@ -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
|
|
88
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
183
|
-
await runCmd('schtasks', ['/Delete', '/TN', taskNameOf(
|
|
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(
|
|
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(
|
|
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(
|
|
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` : '' });
|
package/src/listener/prompt.js
CHANGED
|
@@ -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
|
|
package/src/listener/run.js
CHANGED
|
@@ -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.
|