@nocobase/app-cli-client 0.1.0-beta.4 → 0.1.0-beta.6

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
@@ -4,6 +4,10 @@ A command line for a NocoBase application, branded by the application: it signs
4
4
 
5
5
  Business commands render lists as tables and object responses as named fields. HTTP(S) URLs remain complete for copying. Ordinary text and structured fields retain their compact display. Use `--json` for complete nested results, including Projects invitation links, and for scripts instead of parsing the text layout.
6
6
 
7
+ A branded package with `skills/nb-studio-cli/SKILL.md` also exposes `skills install` and `skills status`. These local commands require neither a login nor a network connection. Install selects detected Claude Code and Codex tools by default, or takes repeated `--tool claude|codex|all`; `--dry-run` computes actions without writing. Claude's destination is `<CLAUDE_CONFIG_DIR or ~/.claude>/skills/nb-studio-cli`; Codex's destination is `~/.agents/skills/nb-studio-cli` regardless of `CODEX_HOME`, following the [official user Skill directory](https://learn.chatgpt.com/docs/build-skills#where-codex-loads-local-skills). Tool detection uses each tool's configuration variable or directory, or its executable on PATH; the shared `.agents/skills` directory alone does not detect Codex. No detected tool yields `SKILLS_NO_TOOL` with explicit installation suggestions.
8
+
9
+ Status compares sorted relative paths and file contents using SHA-256 and reports manual copies, legacy Codex copies in `<CODEX_HOME or ~/.codex>/skills/nb-studio-cli`, and previously registered paths. Only `nb-studio-cli` is managed; install does not create legacy copies. Successful installations are recorded in `<CLI state directory>/skills.json`. Self-update uses the newly installed package (tarball root or npm package root) to synchronize recorded destinations. Skill failures leave the CLI upgrade successful and appear in the update envelope's `warnings` and `result.skills`; manual copies prompt an installation reminder. External upgrades, including npm global installation, require running `skills install` again. Install refuses agent run contexts, while status remains available. See Studio's [static command reference](../../apps/studio/docs/cli/static-commands.md) for the result schema and filesystem constraints.
10
+
7
11
  ```ts
8
12
  import { runAppCli } from '@nocobase/app-cli-client';
9
13
 
@@ -20,6 +24,8 @@ await runAppCli(
20
24
  );
21
25
  ```
22
26
 
27
+ `<bin> acp start` connects the computer it runs on to the signed-in person's agent sessions (`@nocobase/app-plugin-agent-sessions`), so the sessions they start in the application's pages run there with the coding agents installed there; it runs the bridge of `@nocobase/acp-bridge` until stopped with Ctrl+C. `<bin> acp service install` runs the same in the background from every login, as a launchd agent on macOS, a systemd user unit on Linux or a script in the Startup folder on Windows, and starts it again when it fails; it writes in the shell's PATH, the CLI's own variables, proxy and CA variables and the `--pass-env` names, since a service reads no shell configuration. `acp service status` says whether it is installed and connected and where its output goes; `acp service uninstall` removes it. One bridge runs per profile: a second one refuses to start. `--root` limits where sessions may work on this computer, repeated for several directories (the home directory by default), and the person chooses the folders among them in the application. `--name` is the computer's name in the application (its host name by default). The computer's id and key are kept per profile and server under `<home>/acp/`, readable by their owner only: the key, issued when the person first connects the computer, keeps it connected however long the sign-in lasts, until the computer is removed in the application. Connecting needs a person's sign-in: an agent run's credential is refused.
28
+
23
29
  An application whose install script (`/api/agents/dist/installScript` of `@nocobase/app-plugin-agents`) installs its CLI names `selfUpdate` in the configuration, with the root of the CLI's package as it runs (`packageRoot`). The CLI then has `<bin> update`, which asks the signed-in server for the newest version it serves for this platform (`selfUpdate.product`, the command name by default), downloads and checks its SHA-256, or, when the server serves no tarball and names the package on npm (`accept=npm`), installs that exact version with the machine's npm, and switches `<prefix>/current` to it, keeping the previous version; business commands and `login` say on stderr when a newer version is served, asking at most every 12 hours (`<state dir>/cache/update.json`). An installation the script made for the runner (`install.json` `mode: runner`) is the runner's, which updates itself, so `update` refuses it. `@nocobase/app-cli-client/install` is that installation layout (`<prefix>/versions/<version>`, `current`, `<prefix>/node`, `install.json`) and the update itself, without reading any CLI configuration, so a runner shares it. A version directory holds an unpacked tarball, or an npm install (`installNpmVersion`: `npm install --prefix <dir> --no-save --no-audit --no-fund --omit=optional <package>@<version>`, with npm from `NOCOBASE_NPM`, PATH or beside the Node) with the launcher `npmLauncherScript` at `bin/<bin>`, which starts `node_modules/.bin/<bin>` on `<prefix>/node` or the first Node.js 24 it finds; `detectInstallation` takes either root, the version directory or the package inside its `node_modules`, and `applyUpdate` installs an `UpdateTarget` of either kind.
24
30
 
25
31
  `@nocobase/app-cli-client/request` exports `fillRequest`, which turns a manifest command and the values of its parameters into the request it makes. It imports nothing else, so a server that calls its own routes the way the CLI does, such as an online agent's tools in `@nocobase/app-plugin-agents`, builds the same request.
@@ -0,0 +1,39 @@
1
+ export interface AcpServiceOptions {
2
+ /** The CLI's command name, such as `nb-studio`. */
3
+ readonly bin: string;
4
+ /** The program and arguments that start the CLI, such as [node, bin/run.js]. */
5
+ readonly command: readonly string[];
6
+ /** What follows `acp start --service`: the profile, the computer's name and its roots. */
7
+ readonly args: readonly string[];
8
+ /** Where the service's output goes, on macOS and Windows; systemd keeps it in the journal. */
9
+ readonly log: string;
10
+ /** The variables written into the service, with their values. */
11
+ readonly env: Readonly<Record<string, string>>;
12
+ /** Separates the services of several profiles. */
13
+ readonly profile?: string | undefined;
14
+ readonly platform?: NodeJS.Platform;
15
+ readonly home?: string;
16
+ readonly uid?: number;
17
+ }
18
+ export interface AcpServicePlan {
19
+ readonly platform: 'launchd' | 'systemd' | 'startup';
20
+ readonly label: string;
21
+ readonly file: string;
22
+ readonly content: string;
23
+ readonly install: readonly (readonly string[])[];
24
+ readonly uninstall: readonly (readonly string[])[];
25
+ /** Answers with exit code 0 while the service is loaded; none on Windows, where the file is the service. */
26
+ readonly status?: readonly string[];
27
+ }
28
+ /** The proxy and CA variables a service needs to reach the application as the shell does. */
29
+ export declare const NETWORK_ENV: readonly string[];
30
+ /** `names` with their values in `env`, those set; a value with NUL cannot be written into a service. */
31
+ export declare function capture(env: NodeJS.ProcessEnv, names: readonly string[]): Record<string, string>;
32
+ export declare function acpServicePlan(options: AcpServiceOptions): AcpServicePlan;
33
+ /** Writes the service and starts it, replacing one installed before. */
34
+ export declare function installAcpService(plan: AcpServicePlan): Promise<void>;
35
+ /** Stops the service and removes it; on Windows the running bridge is stopped by the caller. */
36
+ export declare function uninstallAcpService(plan: AcpServicePlan): Promise<void>;
37
+ /** Whether the service is loaded (launchd, systemd); undefined where only its file says so. */
38
+ export declare function acpServiceLoaded(plan: AcpServicePlan): Promise<boolean | undefined>;
39
+ //# sourceMappingURL=acp-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acp-service.d.ts","sourceRoot":"","sources":["../src/acp-service.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,0FAA0F;IAC1F,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,8FAA8F;IAC9F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,kDAAkD;IAClD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC;IACjD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC;IACnD,4GAA4G;IAC5G,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,6FAA6F;AAC7F,eAAO,MAAM,WAAW,EAAE,SAAS,MAAM,EAYxC,CAAC;AAEF,wGAAwG;AACxG,wBAAgB,OAAO,CACrB,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAwEzE;AAED,wEAAwE;AACxE,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC3E;AAED,gGAAgG;AAChG,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAK7E;AAED,+FAA+F;AAC/F,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAO9B"}
@@ -0,0 +1,255 @@
1
+ // `<bin> acp start` as a per-user service that starts at login and starts again when it fails: a launchd agent on
2
+ // macOS, a systemd user unit on Linux, and on Windows a script in the Startup folder that runs it without a window. The
3
+ // service runs `acp start --service`, which stops for good, exiting 0, when the application no longer accepts the
4
+ // computer, rather than failing again and again. It gets the installing shell's PATH, the CLI's own variables, proxy and
5
+ // CA variables and the `--pass-env` names with their values now, since a service reads no shell configuration; its
6
+ // files are readable by their owner only. Each plan lists the file it writes and the commands it runs.
7
+ import { execFile, spawn } from 'node:child_process';
8
+ import { chmod, mkdir, rm, writeFile } from 'node:fs/promises';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { promisify } from 'node:util';
12
+ const run = promisify(execFile);
13
+ /** The proxy and CA variables a service needs to reach the application as the shell does. */
14
+ export const NETWORK_ENV = [
15
+ 'HTTP_PROXY',
16
+ 'HTTPS_PROXY',
17
+ 'ALL_PROXY',
18
+ 'NO_PROXY',
19
+ 'http_proxy',
20
+ 'https_proxy',
21
+ 'all_proxy',
22
+ 'no_proxy',
23
+ 'SSL_CERT_FILE',
24
+ 'SSL_CERT_DIR',
25
+ 'NODE_EXTRA_CA_CERTS',
26
+ ];
27
+ /** `names` with their values in `env`, those set; a value with NUL cannot be written into a service. */
28
+ export function capture(env, names) {
29
+ const captured = {};
30
+ for (const name of names) {
31
+ const value = env[name];
32
+ if (value === undefined || value === '')
33
+ continue;
34
+ if (value.includes('\0'))
35
+ throw new Error(`Cannot write ${name} into a service: it holds NUL.`);
36
+ captured[name] = value;
37
+ }
38
+ return captured;
39
+ }
40
+ export function acpServicePlan(options) {
41
+ const platform = options.platform ?? process.platform;
42
+ const home = options.home ?? os.homedir();
43
+ const suffix = options.profile
44
+ ? `-${options.profile.replace(/[^A-Za-z0-9._-]/gu, '_')}`
45
+ : '';
46
+ const name = `${options.bin}-acp${suffix}`;
47
+ const args = [
48
+ ...options.command,
49
+ 'acp',
50
+ 'start',
51
+ '--service',
52
+ ...options.args,
53
+ ];
54
+ if (platform === 'darwin') {
55
+ const label = `com.nocobase.${name}`;
56
+ const file = path.join(home, 'Library', 'LaunchAgents', `${label}.plist`);
57
+ const domain = `gui/${options.uid ?? process.getuid?.() ?? 501}`;
58
+ return {
59
+ platform: 'launchd',
60
+ label,
61
+ file,
62
+ content: plist(label, args, options.env, options.log),
63
+ install: [
64
+ ['launchctl', 'bootout', `${domain}/${label}`],
65
+ ['launchctl', 'bootstrap', domain, file],
66
+ ],
67
+ uninstall: [['launchctl', 'bootout', `${domain}/${label}`]],
68
+ status: ['launchctl', 'print', `${domain}/${label}`],
69
+ };
70
+ }
71
+ if (platform === 'linux') {
72
+ const unit = `${name}.service`;
73
+ return {
74
+ platform: 'systemd',
75
+ label: unit,
76
+ file: path.join(home, '.config', 'systemd', 'user', unit),
77
+ content: systemdUnit(options.bin, args, options.env),
78
+ install: [
79
+ ['systemctl', '--user', 'daemon-reload'],
80
+ ['systemctl', '--user', 'enable', unit],
81
+ ['systemctl', '--user', 'restart', unit],
82
+ ],
83
+ uninstall: [
84
+ ['systemctl', '--user', 'disable', '--now', unit],
85
+ ['systemctl', '--user', 'daemon-reload'],
86
+ ],
87
+ status: ['systemctl', '--user', 'is-active', '--quiet', unit],
88
+ };
89
+ }
90
+ if (platform === 'win32') {
91
+ const appData = process.env.APPDATA ?? path.win32.join(home, 'AppData', 'Roaming');
92
+ const file = path.win32.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${name}.vbs`);
93
+ return {
94
+ platform: 'startup',
95
+ label: name,
96
+ file,
97
+ content: startupScript(args, options.env, options.log),
98
+ install: [['wscript.exe', '//B', '//Nologo', file]],
99
+ uninstall: [],
100
+ };
101
+ }
102
+ throw new Error(`Running as a service is not supported on ${platform}.`);
103
+ }
104
+ /** Writes the service and starts it, replacing one installed before. */
105
+ export async function installAcpService(plan) {
106
+ await mkdir(path.dirname(plan.file), { recursive: true });
107
+ // Only its owner reads it: the variables written into it may hold a proxy's password or a key.
108
+ await writeFile(plan.file, plan.content, { mode: 0o600 });
109
+ await chmod(plan.file, 0o600);
110
+ if (plan.platform === 'startup') {
111
+ // The script starts at the next login by itself; this starts it now, outliving this command.
112
+ const [command = '', ...args] = plan.install[0] ?? [];
113
+ spawn(command, args, {
114
+ detached: true,
115
+ stdio: 'ignore',
116
+ windowsHide: true,
117
+ }).unref();
118
+ return;
119
+ }
120
+ for (const [index, [command = '', ...args]] of plan.install.entries()) {
121
+ // launchd's `bootout` of a service that is not loaded fails, which is fine; it may also still be removing the
122
+ // previous instance right after it, so `bootstrap` is tried again for a few seconds.
123
+ if (plan.platform === 'launchd' && index === 0) {
124
+ await run(command, args).catch(() => undefined);
125
+ continue;
126
+ }
127
+ for (let attempt = 1;; attempt += 1) {
128
+ try {
129
+ await run(command, args);
130
+ break;
131
+ }
132
+ catch (error) {
133
+ if (plan.platform !== 'launchd' || attempt >= 10)
134
+ throw error;
135
+ await new Promise((resolve) => setTimeout(resolve, 500));
136
+ }
137
+ }
138
+ }
139
+ }
140
+ /** Stops the service and removes it; on Windows the running bridge is stopped by the caller. */
141
+ export async function uninstallAcpService(plan) {
142
+ for (const [command = '', ...args] of plan.uninstall)
143
+ // Not loaded is fine: the goal is that it is not.
144
+ await run(command, args).catch(() => undefined);
145
+ await rm(plan.file, { force: true });
146
+ }
147
+ /** Whether the service is loaded (launchd, systemd); undefined where only its file says so. */
148
+ export async function acpServiceLoaded(plan) {
149
+ if (!plan.status)
150
+ return undefined;
151
+ const [command = '', ...args] = plan.status;
152
+ return run(command, args).then(() => true, () => false);
153
+ }
154
+ function xml(value) {
155
+ return value
156
+ .replace(/&/gu, '&amp;')
157
+ .replace(/</gu, '&lt;')
158
+ .replace(/>/gu, '&gt;');
159
+ }
160
+ function plist(label, args, env, log) {
161
+ return `<?xml version="1.0" encoding="UTF-8"?>
162
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
163
+ <plist version="1.0">
164
+ <dict>
165
+ <key>Label</key>
166
+ <string>${xml(label)}</string>
167
+ <key>ProgramArguments</key>
168
+ <array>
169
+ ${args.map((arg) => ` <string>${xml(arg)}</string>`).join('\n')}
170
+ </array>
171
+ <key>EnvironmentVariables</key>
172
+ <dict>
173
+ ${Object.entries(env)
174
+ .map(([key, value]) => ` <key>${xml(key)}</key>\n <string>${xml(value)}</string>`)
175
+ .join('\n')}
176
+ </dict>
177
+ <key>RunAtLoad</key>
178
+ <true/>
179
+ <key>KeepAlive</key>
180
+ <dict>
181
+ <key>SuccessfulExit</key>
182
+ <false/>
183
+ </dict>
184
+ <key>ThrottleInterval</key>
185
+ <integer>10</integer>
186
+ <key>StandardOutPath</key>
187
+ <string>${xml(log)}</string>
188
+ <key>StandardErrorPath</key>
189
+ <string>${xml(log)}</string>
190
+ </dict>
191
+ </plist>
192
+ `;
193
+ }
194
+ function systemdQuote(value) {
195
+ return /[\s'"\\\p{Cc}]/u.test(value)
196
+ ? `"${value.replace(/[\\"\p{Cc}]/gu, (character) => {
197
+ if (character === '\\' || character === '"')
198
+ return `\\${character}`;
199
+ const code = character.charCodeAt(0);
200
+ return code < 128
201
+ ? `\\x${code.toString(16).padStart(2, '0')}`
202
+ : `\\u${code.toString(16).padStart(4, '0')}`;
203
+ })}"`
204
+ : value;
205
+ }
206
+ function systemdUnit(bin, args, env) {
207
+ return `[Unit]
208
+ Description=${bin} agent sessions on this computer
209
+ After=network-online.target
210
+
211
+ [Service]
212
+ ExecStart=${args.map(systemdQuote).join(' ')}
213
+ ${Object.entries(env)
214
+ // Quoted, with `%` doubled so systemd expands no specifier in it.
215
+ .map(([key, value]) => `Environment=${systemdQuote(`${key}=${value}`.replace(/%/gu, '%%'))}`)
216
+ .join('\n')}
217
+ Restart=on-failure
218
+ RestartSec=10
219
+ KillMode=mixed
220
+
221
+ [Install]
222
+ WantedBy=default.target
223
+ `;
224
+ }
225
+ /** `value` as one argument of a cmd.exe command line. */
226
+ function cmdQuote(value) {
227
+ return /^[\w@+=:,./\\-]+$/u.test(value)
228
+ ? value
229
+ : `"${value.replace(/"/gu, '""')}"`;
230
+ }
231
+ /** `value` as a VBScript string literal. */
232
+ function vbsString(value) {
233
+ return `"${value.replace(/"/gu, '""')}"`;
234
+ }
235
+ /**
236
+ * A script Windows runs at login from the Startup folder: it runs the CLI without a window, its output appended to the
237
+ * log, and again 10 seconds after it fails, until the CLI exits 0 or the script is removed.
238
+ */
239
+ function startupScript(args, env, log) {
240
+ const line = `cmd.exe /d /s /c "${args.map(cmdQuote).join(' ')} >> ${cmdQuote(log)} 2>&1"`;
241
+ return [
242
+ "' Runs the agent sessions bridge at login, without a window; remove this file to stop it starting.",
243
+ 'Set shell = CreateObject("WScript.Shell")',
244
+ 'Set files = CreateObject("Scripting.FileSystemObject")',
245
+ 'Set env = shell.Environment("Process")',
246
+ ...Object.entries(env).map(([key, value]) => `env(${vbsString(key)}) = ${vbsString(value)}`),
247
+ 'Do',
248
+ ` code = shell.Run(${vbsString(line)}, 0, True)`,
249
+ ' If code = 0 Or Not files.FileExists(WScript.ScriptFullName) Then Exit Do',
250
+ ' WScript.Sleep 10000',
251
+ 'Loop',
252
+ '',
253
+ ].join('\r\n');
254
+ }
255
+ //# sourceMappingURL=acp-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acp-service.js","sourceRoot":"","sources":["../src/acp-service.ts"],"names":[],"mappings":"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"}
package/dist/acp.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { type Command } from '@oclif/core';
2
+ import type { AppCliConfig } from './config.ts';
3
+ export declare function acpCommands(app: AppCliConfig): Record<string, Command.Class>;
4
+ //# sourceMappingURL=acp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acp.d.ts","sourceRoot":"","sources":["../src/acp.ts"],"names":[],"mappings":"AAUA,OAAO,EAAS,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AAclD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CA2P5E"}
package/dist/acp.js ADDED
@@ -0,0 +1,302 @@
1
+ // `<bin> acp start`: connects this computer to the signed-in person's agent sessions and keeps it connected until it
2
+ // is stopped; `<bin> acp service install` does the same at every login, in the background (`acp-service.ts`). The
3
+ // bridge itself is `@nocobase/acp-bridge`; this command hands it the server, the person's credentials and the file that
4
+ // remembers this computer and its key for the profile, and keeps a second bridge for that profile from starting.
5
+ import { createHash } from 'node:crypto';
6
+ import { existsSync } from 'node:fs';
7
+ import { mkdir, open, readFile, rm } from 'node:fs/promises';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { Flags } from '@oclif/core';
11
+ import { BridgeStopped, runBridge } from '@nocobase/acp-bridge';
12
+ import { EXIT_CODES } from '@nocobase/agent-protocol';
13
+ import { acpServiceLoaded, acpServicePlan, capture, installAcpService, NETWORK_ENV, uninstallAcpService, } from "./acp-service.js";
14
+ import { currentSession } from "./dynamic/session.js";
15
+ import { detectInstallation } from "./install.js";
16
+ import { AppCommand, UsageError } from "./lib/command.js";
17
+ import { globalFlags } from "./lib/globals.js";
18
+ export function acpCommands(app) {
19
+ const placeFlags = {
20
+ name: Flags.string({
21
+ description: "The computer's name in the application; its host name by default.",
22
+ }),
23
+ root: Flags.string({
24
+ description: 'A directory sessions may work in; repeat for several. Your home directory by default.',
25
+ multiple: true,
26
+ }),
27
+ };
28
+ /** The signed-in person, or a usage error saying to sign in. */
29
+ async function signedIn() {
30
+ const session = await currentSession();
31
+ if (session?.kind !== 'user') {
32
+ throw new UsageError(session
33
+ ? "An agent's run cannot connect a computer. Run this outside the run's working directory."
34
+ : `Not signed in. Run \`${app.bin} login\` first.`, EXIT_CODES.auth);
35
+ }
36
+ return session;
37
+ }
38
+ /**
39
+ * The service for the current profile, starting the CLI as it was started now (`root` is its package's); `hidden`
40
+ * leaves the values of the variables written into it out, for printing.
41
+ */
42
+ function servicePlan(root, home, flags, hidden = false) {
43
+ const profile = globalFlags().profile;
44
+ const prefix = app.envPrefix;
45
+ const env = {
46
+ PATH: process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin',
47
+ ...capture(process.env, [
48
+ ...(app.homeEnv ? [app.homeEnv] : []),
49
+ ...(app.keychainEnv ? [app.keychainEnv] : []),
50
+ ...(prefix
51
+ ? [`${prefix}_SERVER`, `${prefix}_API_KEY`, `${prefix}_PROFILE`]
52
+ : []),
53
+ ...NETWORK_ENV,
54
+ ...(flags['pass-env'] ?? []),
55
+ ]),
56
+ };
57
+ return acpServicePlan({
58
+ bin: app.bin,
59
+ command: selfCommand(root),
60
+ args: [
61
+ ...(profile ? ['--profile', profile] : []),
62
+ ...(flags.name ? ['--name', flags.name] : []),
63
+ ...(flags.root ?? []).flatMap((root) => [
64
+ '--root',
65
+ path.resolve(expandHome(root)),
66
+ ]),
67
+ ],
68
+ log: path.join(home, 'acp', 'service.log'),
69
+ env: hidden
70
+ ? Object.fromEntries(Object.keys(env).map((name) => [name, '<hidden>']))
71
+ : env,
72
+ profile,
73
+ });
74
+ }
75
+ return {
76
+ 'acp:start': class AcpStart extends AppCommand {
77
+ static summary = 'Run your agent sessions on this computer.';
78
+ static description = `Connects this computer to ${app.displayName} as the signed-in person, so the sessions you start there run ` +
79
+ 'here with the coding agents installed here, in the directories you allow. It stays connected until you stop it ' +
80
+ `with Ctrl+C; stopping it stops the agents it started. To keep it running in the background from every login, ` +
81
+ `run \`${app.bin} acp service install\` instead.`;
82
+ static examples = [
83
+ `${app.bin} acp start`,
84
+ `${app.bin} acp start --name "Work laptop" --root ~/code`,
85
+ ];
86
+ static flags = {
87
+ ...placeFlags,
88
+ service: Flags.boolean({
89
+ description: 'Run as the background service does: stop for good when the application no longer accepts this computer.',
90
+ hidden: true,
91
+ }),
92
+ };
93
+ async run() {
94
+ const { flags } = await this.parse(AcpStart);
95
+ const session = await signedIn();
96
+ const stateFile = stateFileOf(this.paths.home, session);
97
+ const release = await holdLock(`${stateFile}.lock`, app.bin);
98
+ const stop = new AbortController();
99
+ const onSignal = () => stop.abort();
100
+ process.once('SIGINT', onSignal);
101
+ process.once('SIGTERM', onSignal);
102
+ try {
103
+ await runBridge({
104
+ server: session.server,
105
+ headers: session.headers,
106
+ client: { name: app.bin, version: this.config.version },
107
+ stateFile,
108
+ ...(flags.name ? { name: flags.name } : {}),
109
+ ...(flags.root
110
+ ? {
111
+ roots: flags.root.map((root) => path.resolve(expandHome(root))),
112
+ }
113
+ : {}),
114
+ reconnect: !flags.service,
115
+ signal: stop.signal,
116
+ log: (message) => this.log(flags.service ? `${timestamp()} ${message}` : message),
117
+ });
118
+ this.log('Disconnected.');
119
+ }
120
+ catch (error) {
121
+ if (!(error instanceof BridgeStopped))
122
+ throw error;
123
+ // The service stops for good, exiting 0, rather than being started again to be refused again.
124
+ if (flags.service) {
125
+ this.log(`${timestamp()} ${error.message}`);
126
+ return;
127
+ }
128
+ throw new UsageError(error.message, EXIT_CODES.auth);
129
+ }
130
+ finally {
131
+ process.off('SIGINT', onSignal);
132
+ process.off('SIGTERM', onSignal);
133
+ await release();
134
+ }
135
+ }
136
+ },
137
+ 'acp:service:install': class AcpServiceInstall extends AppCommand {
138
+ static summary = 'Keep this computer connected in the background, from every login.';
139
+ static description = `Installs \`${app.bin} acp start\` as a service of your user: a launchd agent on macOS, a systemd user unit on ` +
140
+ 'Linux, a script in the Startup folder on Windows. It starts now and at every login, and again when it fails. ' +
141
+ "A service does not read your shell configuration, so this shell's PATH (where the agents are found), proxy and " +
142
+ 'CA variables and the --pass-env names are written into it with their values now; install again after ' +
143
+ 'changing one, or after installing an agent somewhere new.';
144
+ static supportsDryRun = true;
145
+ static examples = [
146
+ `${app.bin} acp service install`,
147
+ `${app.bin} acp service install --root ~/code --pass-env ANTHROPIC_API_KEY`,
148
+ ];
149
+ static flags = {
150
+ ...placeFlags,
151
+ 'pass-env': Flags.string({
152
+ description: 'An environment variable the agents need, such as an API key kept in your shell; repeat for several.',
153
+ multiple: true,
154
+ }),
155
+ };
156
+ async run() {
157
+ const { flags } = await this.parse(AcpServiceInstall);
158
+ const session = await signedIn();
159
+ const plan = servicePlan(this.config.root, this.paths.home, flags);
160
+ if (this.dryRun) {
161
+ this.log(`# ${plan.file}`);
162
+ this.log(servicePlan(this.config.root, this.paths.home, flags, true).content);
163
+ return { file: plan.file, installed: false };
164
+ }
165
+ const lock = `${stateFileOf(this.paths.home, session)}.lock`;
166
+ const running = await lockHolder(lock);
167
+ if (running !== undefined && !(await acpServiceLoaded(plan)))
168
+ throw new UsageError(`This computer is already connected by \`${app.bin} acp start\` (process ${running}). Stop it first, then install the service.`);
169
+ if (plan.platform === 'startup')
170
+ await stopHolder(lock);
171
+ await installAcpService(plan);
172
+ this.log(`Installed ${plan.file}: this computer stays connected from every login.`);
173
+ if (plan.platform === 'systemd')
174
+ this.log('A user service stops when you log out; to keep it running then, run `loginctl enable-linger`.');
175
+ this.log(`See how it runs with \`${app.bin} acp service status\`; stop it with \`${app.bin} acp service uninstall\`.`);
176
+ return { file: plan.file, installed: true };
177
+ }
178
+ },
179
+ 'acp:service:uninstall': class AcpServiceUninstall extends AppCommand {
180
+ static summary = 'Stop keeping this computer connected in the background.';
181
+ static description = 'Stops the service `acp service install` installed and removes it. The computer stays in the application, offline, until you remove it there or connect it again.';
182
+ async run() {
183
+ const plan = servicePlan(this.config.root, this.paths.home, {});
184
+ await uninstallAcpService(plan);
185
+ const session = await currentSession();
186
+ // On Windows the file only starts the bridge; the bridge running now is stopped here.
187
+ if (plan.platform === 'startup' && session?.kind === 'user')
188
+ await stopHolder(`${stateFileOf(this.paths.home, session)}.lock`);
189
+ this.log(`Removed ${plan.file}.`);
190
+ return { file: plan.file };
191
+ }
192
+ },
193
+ 'acp:service:status': class AcpServiceStatus extends AppCommand {
194
+ static summary = 'Show whether this computer is connected in the background.';
195
+ async run() {
196
+ const plan = servicePlan(this.config.root, this.paths.home, {});
197
+ const session = await currentSession();
198
+ const installed = existsSync(plan.file);
199
+ const holder = session?.kind === 'user'
200
+ ? await lockHolder(`${stateFileOf(this.paths.home, session)}.lock`)
201
+ : undefined;
202
+ const loaded = installed ? await acpServiceLoaded(plan) : false;
203
+ const log = path.join(this.paths.home, 'acp', 'service.log');
204
+ this.log(installed
205
+ ? `Installed: ${plan.file}${loaded === false ? ' (not loaded)' : ''}`
206
+ : `Not installed. Install it with \`${app.bin} acp service install\`.`);
207
+ this.log(holder === undefined
208
+ ? 'Not connected now.'
209
+ : `Connected now (process ${holder}).`);
210
+ if (plan.platform === 'systemd')
211
+ this.log(`Its output: journalctl --user -u ${plan.label}`);
212
+ else
213
+ this.log(`Its output: ${log}`);
214
+ return {
215
+ installed,
216
+ running: holder !== undefined,
217
+ file: plan.file,
218
+ log,
219
+ };
220
+ }
221
+ },
222
+ };
223
+ }
224
+ /** Where the bridge remembers this computer and its key, for the profile and server. */
225
+ function stateFileOf(home, session) {
226
+ return path.join(home, 'acp', `${createHash('sha256').update(session.cacheKey).update(session.server).digest('hex').slice(0, 16)}.json`);
227
+ }
228
+ /**
229
+ * Makes this process the one running the bridge for the state file `lock` stands beside, or refuses when another
230
+ * running process is; answers what lets go of it.
231
+ */
232
+ async function holdLock(lock, bin) {
233
+ await mkdir(path.dirname(lock), { recursive: true, mode: 0o700 });
234
+ for (;;) {
235
+ try {
236
+ const handle = await open(lock, 'wx', 0o600);
237
+ await handle.writeFile(String(process.pid));
238
+ await handle.close();
239
+ return () => rm(lock, { force: true });
240
+ }
241
+ catch (error) {
242
+ if (error.code !== 'EEXIST')
243
+ throw error;
244
+ }
245
+ const holder = await lockHolder(lock);
246
+ if (holder !== undefined)
247
+ throw new UsageError(`This computer is already connected (process ${holder}), in a terminal or by the background service (\`${bin} acp service status\`).`);
248
+ // Left behind by a bridge that did not stop cleanly.
249
+ await rm(lock, { force: true });
250
+ }
251
+ }
252
+ /** The running process holding `lock`, if any. */
253
+ async function lockHolder(lock) {
254
+ const pid = Number(await readFile(lock, 'utf8').catch(() => ''));
255
+ if (!Number.isInteger(pid) || pid <= 0)
256
+ return undefined;
257
+ try {
258
+ process.kill(pid, 0);
259
+ return pid;
260
+ }
261
+ catch (error) {
262
+ return error.code === 'EPERM' ? pid : undefined;
263
+ }
264
+ }
265
+ /** Stops the bridge holding `lock`, with what it started. */
266
+ async function stopHolder(lock) {
267
+ const pid = await lockHolder(lock);
268
+ if (pid === undefined)
269
+ return;
270
+ if (process.platform === 'win32') {
271
+ const { execFile } = await import('node:child_process');
272
+ await new Promise((resolve) => {
273
+ execFile('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true }, () => resolve());
274
+ });
275
+ }
276
+ else
277
+ process.kill(pid, 'SIGTERM');
278
+ }
279
+ /**
280
+ * How a service starts this CLI: this Node and this entry, through `<prefix>/current` when the CLI was installed by
281
+ * the application's install script, so that the service runs whichever version an update made current.
282
+ */
283
+ function selfCommand(root) {
284
+ const installation = detectInstallation(root);
285
+ const throughCurrent = (file) => installation && file.startsWith(installation.versionDir)
286
+ ? path.join(installation.current, file.slice(installation.versionDir.length))
287
+ : file;
288
+ return [
289
+ throughCurrent(process.execPath),
290
+ ...process.execArgv,
291
+ throughCurrent(process.argv[1] ?? ''),
292
+ ];
293
+ }
294
+ function timestamp() {
295
+ return new Date().toISOString();
296
+ }
297
+ function expandHome(dir) {
298
+ return dir === '~' || dir.startsWith('~/') || dir.startsWith('~\\')
299
+ ? path.join(os.homedir(), dir.slice(1))
300
+ : dir;
301
+ }
302
+ //# sourceMappingURL=acp.js.map