@anhlp/alp 0.0.1-beta.3 → 0.0.1-beta.4
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 +18 -16
- package/dist/acp.js +37 -4
- package/dist/alpd.js +10350 -3346
- package/docs/modular-monorepo.md +17 -13
- package/package.json +8 -6
- package/packages/cli/dist/THIRD-PARTY-NOTICES.txt +25 -0
- package/packages/cli/dist/acp/main.js +1 -1
- package/packages/cli/dist/acp.js +37 -4
- package/packages/cli/dist/alp-web/THIRD-PARTY-NOTICES.txt +2590 -0
- package/packages/cli/dist/alp-web/alp-boundary.json +15 -0
- package/packages/cli/dist/alp-web/apple-touch-icon.png +0 -0
- package/packages/cli/dist/alp-web/assets/TerminalPanel-DLuoa74B.css +1 -0
- package/packages/cli/dist/alp-web/assets/TerminalPanel-PrU2l3u5.js +16 -0
- package/packages/cli/dist/alp-web/assets/index-BJdBTDFX.css +1 -0
- package/packages/cli/dist/alp-web/assets/index-DpfiPv5s.js +52 -0
- package/packages/cli/dist/alp-web/favicon-attention.svg +6 -0
- package/packages/cli/dist/alp-web/favicon-running.svg +6 -0
- package/packages/cli/dist/alp-web/favicon.svg +5 -0
- package/packages/cli/dist/alp-web/index.html +20 -0
- package/packages/cli/dist/alp-web.js +9640 -0
- package/packages/cli/dist/alpd.js +10350 -3346
- package/packages/cli/dist/backup.js +2 -2
- package/packages/cli/dist/build-manifest.json +99 -1
- package/packages/cli/dist/cli.js +4 -2
- package/packages/cli/dist/interfaces.d.ts +9 -1
- package/packages/cli/dist/interfaces.js +99 -39
- package/packages/cli/dist/schedules.d.ts +2 -0
- package/packages/cli/dist/schedules.js +45 -0
- package/packages/cli/dist/sessions.d.ts +3 -1
- package/packages/cli/dist/sessions.js +22 -11
- package/packages/cli/dist/terminal.d.ts +8 -0
- package/packages/cli/dist/terminal.js +79 -0
- package/packages/cli/dist/usage.d.ts +1 -1
- package/packages/cli/dist/usage.js +8 -2
- package/packages/cli/dist/web-service.d.ts +38 -0
- package/packages/cli/dist/web-service.js +119 -0
- package/packages/client/README.md +6 -1
- package/packages/client/dist/index.d.ts +37 -2
- package/packages/client/dist/index.js +55 -22
- package/packages/client/dist/integration-lock.d.ts +35 -0
- package/packages/client/dist/integration-lock.js +179 -0
- package/packages/client/dist/portable.d.ts +1 -0
- package/packages/client/dist/portable.js +7 -1
- package/packages/client/dist/rpc/registry.d.ts +211 -0
- package/packages/client/dist/rpc/registry.js +145 -0
- package/packages/client/dist/supervise.d.ts +2 -0
- package/packages/client/dist/supervise.js +4 -4
- package/packages/core/dist/backup.js +103 -4
- package/packages/core/dist/index.d.ts +1 -0
- package/packages/core/dist/index.js +2 -0
- package/packages/core/dist/migrations.js +1 -1
- package/packages/core/dist/user-worktree.d.ts +29 -0
- package/packages/core/dist/user-worktree.js +242 -0
- package/packages/core/dist/validation.js +3 -2
|
@@ -7,7 +7,7 @@ import { acquireDaemonLock, alpHome, assertPrivateDirectory, daemonPaths, lockAl
|
|
|
7
7
|
import { daemonHeld, holdDaemon, releaseHold } from "@anhlp/alp-client/supervise";
|
|
8
8
|
import { start, stop } from "./daemon.js";
|
|
9
9
|
import { UsageError } from "./usage.js";
|
|
10
|
-
var VERSION = true ? "0.0.1-beta.
|
|
10
|
+
var VERSION = true ? "0.0.1-beta.4" : "dev";
|
|
11
11
|
var BY = `alp ${VERSION}`;
|
|
12
12
|
var size = (bytes) => bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : bytes >= 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${bytes} B`;
|
|
13
13
|
var stamp = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
@@ -138,7 +138,7 @@ async function restoreCommand(args) {
|
|
|
138
138
|
for (const { root, reason } of result.skipped) console.log(` skipped ${root}: ${reason}`);
|
|
139
139
|
for (const warning of result.warnings) console.log(` note: ${warning}`);
|
|
140
140
|
if (result.stateSchema !== null && result.stateSchema !== void 0) console.log(" sessions that were working when the backup was made wait for you; alpd continues none by itself");
|
|
141
|
-
if (result.safety) console.log(`the data it replaced is in ${result.safety}; alp restore ${result.safety}
|
|
141
|
+
if (result.safety) console.log(`the data it replaced is in ${result.safety}; alp restore ${result.safety} restores that data with the same workspace authority checks`);
|
|
142
142
|
}
|
|
143
143
|
export {
|
|
144
144
|
backupCommand,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.1-beta.
|
|
2
|
+
"version": "0.0.1-beta.4",
|
|
3
3
|
"entries": {
|
|
4
4
|
"packages/cli/dist/alpd.js": {
|
|
5
5
|
"inputs": [
|
|
@@ -11,16 +11,19 @@
|
|
|
11
11
|
"packages/client/src/rpc/framing.js",
|
|
12
12
|
"packages/client/src/rpc/handshake.js",
|
|
13
13
|
"packages/client/src/rpc/reconnect.js",
|
|
14
|
+
"packages/client/src/rpc/registry.js",
|
|
14
15
|
"packages/client/src/rpc/timers.js",
|
|
15
16
|
"packages/client/src/websocket.js",
|
|
16
17
|
"packages/core/src/adapter.js",
|
|
17
18
|
"packages/core/src/atomic.js",
|
|
18
19
|
"packages/core/src/backup.js",
|
|
20
|
+
"packages/core/src/beads.js",
|
|
19
21
|
"packages/core/src/delegation.js",
|
|
20
22
|
"packages/core/src/errors.js",
|
|
21
23
|
"packages/core/src/formulas.js",
|
|
22
24
|
"packages/core/src/hook-run.js",
|
|
23
25
|
"packages/core/src/hooks.js",
|
|
26
|
+
"packages/core/src/index.js",
|
|
24
27
|
"packages/core/src/init.js",
|
|
25
28
|
"packages/core/src/library-edit.js",
|
|
26
29
|
"packages/core/src/library.js",
|
|
@@ -40,18 +43,30 @@
|
|
|
40
43
|
"packages/core/src/teams.js",
|
|
41
44
|
"packages/core/src/template-path.js",
|
|
42
45
|
"packages/core/src/trust.js",
|
|
46
|
+
"packages/core/src/upgrade.js",
|
|
43
47
|
"packages/core/src/user-settings.js",
|
|
48
|
+
"packages/core/src/user-worktree.js",
|
|
44
49
|
"packages/core/src/validation.js",
|
|
45
50
|
"packages/core/src/verify.js",
|
|
46
51
|
"packages/core/src/workflow.js",
|
|
52
|
+
"packages/daemon/src/attachments.ts",
|
|
53
|
+
"packages/daemon/src/conversation.ts",
|
|
47
54
|
"packages/daemon/src/files.ts",
|
|
55
|
+
"packages/daemon/src/git.ts",
|
|
48
56
|
"packages/daemon/src/gitignore.ts",
|
|
57
|
+
"packages/daemon/src/history.ts",
|
|
58
|
+
"packages/daemon/src/lifecycle.ts",
|
|
49
59
|
"packages/daemon/src/lock.ts",
|
|
50
60
|
"packages/daemon/src/main.ts",
|
|
51
61
|
"packages/daemon/src/operations.ts",
|
|
62
|
+
"packages/daemon/src/registry.ts",
|
|
52
63
|
"packages/daemon/src/rpc-error.ts",
|
|
64
|
+
"packages/daemon/src/scheduler.ts",
|
|
65
|
+
"packages/daemon/src/schedules.ts",
|
|
53
66
|
"packages/daemon/src/server.ts",
|
|
54
67
|
"packages/daemon/src/store.ts",
|
|
68
|
+
"packages/daemon/src/terminals.ts",
|
|
69
|
+
"packages/daemon/src/worktrees.ts",
|
|
55
70
|
"packages/protocol/src/contract-v1.js",
|
|
56
71
|
"packages/protocol/src/contract.js",
|
|
57
72
|
"packages/protocol/src/index.ts",
|
|
@@ -59,11 +74,15 @@
|
|
|
59
74
|
"packages/protocol/src/wire.js",
|
|
60
75
|
"packages/runtime/src/acp-transport.ts",
|
|
61
76
|
"packages/runtime/src/antigravity-transport.ts",
|
|
77
|
+
"packages/runtime/src/assignment-results.ts",
|
|
78
|
+
"packages/runtime/src/attachments.ts",
|
|
62
79
|
"packages/runtime/src/board-domain.ts",
|
|
63
80
|
"packages/runtime/src/board.ts",
|
|
64
81
|
"packages/runtime/src/catalog.ts",
|
|
65
82
|
"packages/runtime/src/claude-transport.ts",
|
|
83
|
+
"packages/runtime/src/configure.ts",
|
|
66
84
|
"packages/runtime/src/contracts.ts",
|
|
85
|
+
"packages/runtime/src/conversation.ts",
|
|
67
86
|
"packages/runtime/src/github.ts",
|
|
68
87
|
"packages/runtime/src/handoff.ts",
|
|
69
88
|
"packages/runtime/src/index.ts",
|
|
@@ -94,6 +113,7 @@
|
|
|
94
113
|
"node:crypto",
|
|
95
114
|
"node:fs",
|
|
96
115
|
"node:fs/promises",
|
|
116
|
+
"node:module",
|
|
97
117
|
"node:net",
|
|
98
118
|
"node:os",
|
|
99
119
|
"node:path",
|
|
@@ -120,6 +140,7 @@
|
|
|
120
140
|
"packages/client/src/rpc/framing.js",
|
|
121
141
|
"packages/client/src/rpc/handshake.js",
|
|
122
142
|
"packages/client/src/rpc/reconnect.js",
|
|
143
|
+
"packages/client/src/rpc/registry.js",
|
|
123
144
|
"packages/client/src/rpc/timers.js",
|
|
124
145
|
"packages/client/src/service.js",
|
|
125
146
|
"packages/client/src/supervise.js",
|
|
@@ -137,6 +158,83 @@
|
|
|
137
158
|
"node:path",
|
|
138
159
|
"node:stream"
|
|
139
160
|
]
|
|
161
|
+
},
|
|
162
|
+
"packages/cli/dist/alp-web.js": {
|
|
163
|
+
"inputs": [
|
|
164
|
+
"apps/gateway/src/config.js",
|
|
165
|
+
"apps/gateway/src/connection.js",
|
|
166
|
+
"apps/gateway/src/credentials.js",
|
|
167
|
+
"apps/gateway/src/index.js",
|
|
168
|
+
"apps/gateway/src/log.js",
|
|
169
|
+
"apps/gateway/src/policy.js",
|
|
170
|
+
"apps/gateway/src/server.js",
|
|
171
|
+
"apps/gateway/src/tickets.js",
|
|
172
|
+
"apps/gateway/src/upstream.js",
|
|
173
|
+
"packages/cli/src/web/grant.js",
|
|
174
|
+
"packages/cli/src/web/main.js",
|
|
175
|
+
"packages/cli/src/web/serve.js",
|
|
176
|
+
"packages/client/src/command.js",
|
|
177
|
+
"packages/client/src/index.js",
|
|
178
|
+
"packages/client/src/integration-lock.js",
|
|
179
|
+
"packages/client/src/ipc.js",
|
|
180
|
+
"packages/client/src/portable.js",
|
|
181
|
+
"packages/client/src/rpc/client.js",
|
|
182
|
+
"packages/client/src/rpc/errors.js",
|
|
183
|
+
"packages/client/src/rpc/framing.js",
|
|
184
|
+
"packages/client/src/rpc/handshake.js",
|
|
185
|
+
"packages/client/src/rpc/reconnect.js",
|
|
186
|
+
"packages/client/src/rpc/registry.js",
|
|
187
|
+
"packages/client/src/rpc/timers.js",
|
|
188
|
+
"packages/client/src/service.js",
|
|
189
|
+
"packages/client/src/supervise.js",
|
|
190
|
+
"packages/client/src/websocket.js",
|
|
191
|
+
"packages/core/src/atomic.js",
|
|
192
|
+
"packages/core/src/errors.js",
|
|
193
|
+
"packages/core/src/library-edit.js",
|
|
194
|
+
"packages/core/src/library.js",
|
|
195
|
+
"packages/core/src/log.js",
|
|
196
|
+
"packages/core/src/models.js",
|
|
197
|
+
"packages/core/src/permissions.js",
|
|
198
|
+
"packages/core/src/providers.js",
|
|
199
|
+
"packages/core/src/resolver.js",
|
|
200
|
+
"packages/core/src/safe-path.js",
|
|
201
|
+
"packages/core/src/shell.js",
|
|
202
|
+
"packages/core/src/teams.js",
|
|
203
|
+
"packages/core/src/template-path.js",
|
|
204
|
+
"packages/core/src/validation.js",
|
|
205
|
+
"packages/core/src/verify.js",
|
|
206
|
+
"packages/protocol/src/contract-v1.js",
|
|
207
|
+
"packages/protocol/src/contract.js",
|
|
208
|
+
"packages/protocol/src/index.ts",
|
|
209
|
+
"packages/protocol/src/validate.js",
|
|
210
|
+
"packages/protocol/src/wire.js"
|
|
211
|
+
],
|
|
212
|
+
"external": [
|
|
213
|
+
"buffer",
|
|
214
|
+
"bufferutil",
|
|
215
|
+
"crypto",
|
|
216
|
+
"events",
|
|
217
|
+
"http",
|
|
218
|
+
"https",
|
|
219
|
+
"net",
|
|
220
|
+
"node:child_process",
|
|
221
|
+
"node:crypto",
|
|
222
|
+
"node:fs",
|
|
223
|
+
"node:fs/promises",
|
|
224
|
+
"node:http",
|
|
225
|
+
"node:https",
|
|
226
|
+
"node:net",
|
|
227
|
+
"node:os",
|
|
228
|
+
"node:path",
|
|
229
|
+
"node:url",
|
|
230
|
+
"node:util",
|
|
231
|
+
"stream",
|
|
232
|
+
"tls",
|
|
233
|
+
"url",
|
|
234
|
+
"utf-8-validate",
|
|
235
|
+
"util",
|
|
236
|
+
"zlib"
|
|
237
|
+
]
|
|
140
238
|
}
|
|
141
239
|
}
|
|
142
240
|
}
|
package/packages/cli/dist/cli.js
CHANGED
|
@@ -9,14 +9,16 @@ import { project, permissionsCommand, trustCommand, language, verify, doctor } f
|
|
|
9
9
|
import { daemon } from "./daemon.js";
|
|
10
10
|
import { run, ps, attach, send, questions, answer, top, log, board, pause, resume, recall, interrupt } from "./sessions.js";
|
|
11
11
|
import { libraryCommand, editCommand, teamsCommand } from "./library.js";
|
|
12
|
+
import { scheduleCommand } from "./schedules.js";
|
|
12
13
|
import { tasksCommand, formulaCommand, taskCommand } from "./tasks.js";
|
|
13
14
|
import { webCommand, acpCommand } from "./interfaces.js";
|
|
14
15
|
import { backupCommand, restoreCommand } from "./backup.js";
|
|
15
|
-
|
|
16
|
+
import { terminalCommand } from "./terminal.js";
|
|
17
|
+
var commands = { schedule: scheduleCommand, setup: (args2) => setup(args2, alpHome()), init: (args2) => project("init", args2), upgrade: (args2) => project("upgrade", args2), update: (args2) => project("upgrade", args2), daemon, doctor, run, ps, top, attach, send, questions, answer, log, board, tasks: tasksCommand, task: taskCommand, formula: formulaCommand, permissions: permissionsCommand, agents: (args2) => libraryCommand("agents", args2), skills: (args2) => libraryCommand("skills", args2), mcp: (args2) => ["add", "rm", "mv", "cp", "test", "show", "edit"].includes(args2[0]) ? editCommand("mcp", args2) : libraryCommand("mcp", args2), hooks: (args2) => libraryCommand("hooks", args2), providers: (args2) => libraryCommand("providers", args2), provider: (args2) => editCommand("providers", args2), teams: teamsCommand, trust: trustCommand, agent: (args2) => editCommand("agents", args2), team: (args2) => editCommand("teams", args2), skill: (args2) => editCommand("skills", args2), hook: (args2) => editCommand("hooks", args2), verify, recall, pause, resume, language, interrupt, acp: acpCommand, web: webCommand, backup: backupCommand, restore: restoreCommand, terminal: terminalCommand };
|
|
16
18
|
var [command, ...args] = process.argv.slice(2);
|
|
17
19
|
try {
|
|
18
20
|
if (command === "--version") {
|
|
19
|
-
console.log(true ? "0.0.1-beta.
|
|
21
|
+
console.log(true ? "0.0.1-beta.4" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
|
|
20
22
|
} else {
|
|
21
23
|
if (!Object.hasOwn(commands, command)) throw new UsageError();
|
|
22
24
|
await commands[command](args);
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starts the Paseo integration. For one release, a home that still has only the descriptor
|
|
3
|
+
* registered before the rename (`integrations/web.json`) keeps working, with a hint to re-register.
|
|
4
|
+
* Exported for tests.
|
|
5
|
+
* @param {string} home
|
|
6
|
+
*/
|
|
7
|
+
export function startPaseoIntegration(home: string): Promise<any>;
|
|
1
8
|
/**
|
|
2
9
|
* alp web pair: the link that pairs Paseo's app on another device with alpd through Paseo's relay
|
|
3
|
-
* (ALPD §64). The device then signs in with the
|
|
10
|
+
* (ALPD §64). The device then signs in with the password this command prints (the token kept in
|
|
11
|
+
* ~/.alp/paseo.json).
|
|
4
12
|
*/
|
|
5
13
|
/**
|
|
6
14
|
* `web.pairing` is an extension the integration registers on its alpd connection. Right after an
|
|
@@ -1,55 +1,111 @@
|
|
|
1
1
|
// packages/cli/src/interfaces.js
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
-
import { alpHome,
|
|
6
|
+
import { alpHome, discoverIntegration, startIntegration } from "@anhlp/alp-client/index";
|
|
7
7
|
import { UsageError } from "./usage.js";
|
|
8
8
|
import { built, start, running } from "./daemon.js";
|
|
9
9
|
import { DAEMON_ENTRY, distributionEntry } from "./paths.js";
|
|
10
|
+
import { alpWebEntry, refusal, stopPreRenamePaseo, stopWebService, webOwner } from "./web-service.js";
|
|
11
|
+
var PASEO = "paseo";
|
|
12
|
+
var WEB = "web";
|
|
13
|
+
async function startPaseoIntegration(home) {
|
|
14
|
+
const entry = await discoverIntegration(PASEO, { home });
|
|
15
|
+
if (!entry) {
|
|
16
|
+
const legacy = path.join(home, "integrations", "web.json");
|
|
17
|
+
const legacyEntry = await legacyDescriptorEntry(legacy, home);
|
|
18
|
+
if (legacyEntry) {
|
|
19
|
+
console.error(`Using the Paseo integration registered before its rename (${legacy}); run \`alp-paseo-integration --register\` to register it again.`);
|
|
20
|
+
return startIntegration({ name: PASEO, entry: legacyEntry, home });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return startIntegration({ name: PASEO, home });
|
|
24
|
+
}
|
|
25
|
+
async function legacyDescriptorEntry(file, home) {
|
|
26
|
+
const raw = await readFile(file, "utf8").then((text) => JSON.parse(text)).catch(() => void 0);
|
|
27
|
+
const isFile = (candidate) => stat(candidate).then((info) => info.isFile(), () => false);
|
|
28
|
+
for (const field of ["main", "entry", "path", "command"]) {
|
|
29
|
+
const value = raw?.[field];
|
|
30
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
31
|
+
const candidates = path.isAbsolute(value) ? [value] : [path.resolve(path.dirname(file), value), path.resolve(home, value)];
|
|
32
|
+
for (const candidate of candidates) if (await isFile(candidate)) return candidate;
|
|
33
|
+
}
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
async function projectArgs() {
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
return await stat(path.join(cwd, ".alp")).then((info) => info.isDirectory(), () => false) ? ["--project", cwd] : [];
|
|
39
|
+
}
|
|
40
|
+
async function webDisabled(home) {
|
|
41
|
+
const settings = await readFile(path.join(home, "settings.json"), "utf8").then((text) => JSON.parse(text), () => void 0);
|
|
42
|
+
return settings?.web?.enabled === false;
|
|
43
|
+
}
|
|
44
|
+
var DISABLED = 'alp web is disabled by settings ("web.enabled": false).';
|
|
45
|
+
async function serveWeb(home, entry) {
|
|
46
|
+
await stopPreRenamePaseo(home);
|
|
47
|
+
const held = await webOwner(home);
|
|
48
|
+
const published = await readFile(path.join(home, "web.json"), "utf8").then((text) => JSON.parse(text), () => void 0);
|
|
49
|
+
const claimed = published?.app === "alp-web" && published.pid === await readFile(path.join(home, "integrations", "web.lock"), "utf8").then((text) => JSON.parse(text)?.pid, () => void 0);
|
|
50
|
+
if ("reason" in held && held.alive && held.kind === "foreign" && !claimed) throw new Error(`${refusal(home)} (${held.reason}). Stop it, then run alp web again.`);
|
|
51
|
+
let lock;
|
|
52
|
+
try {
|
|
53
|
+
lock = await startIntegration({ name: WEB, entry, home, args: await projectArgs() });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const owner = await webOwner(home);
|
|
56
|
+
if ("reason" in owner && owner.alive) throw new Error(`${error?.message ?? error}
|
|
57
|
+
${refusal(home)} (${owner.reason}). If it is not alp web, stop it, then run alp web again.`, { cause: error });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const info = await readFile(path.join(home, "web.json"), "utf8").then((text) => JSON.parse(text), () => void 0);
|
|
61
|
+
if (info?.app !== "alp-web" || info.pid !== lock.pid) {
|
|
62
|
+
const owner = await webOwner(home);
|
|
63
|
+
throw new Error(`${refusal(home)}${"reason" in owner ? ` (${owner.reason})` : ""}. Stop pid ${lock.pid}, then run alp web again.`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof info.url !== "string" || typeof info.token !== "string") throw new Error(`alp web published no address; see ${path.join(home, "logs", "integrations", "alp-web.log")}`);
|
|
66
|
+
return `${info.url}#token=${info.token}`;
|
|
67
|
+
}
|
|
10
68
|
async function webCommand(args) {
|
|
11
69
|
if (args[0] === "pair") return pairCommand(args.slice(1));
|
|
70
|
+
if (args[0] === "rotate") return rotateCommand(args.slice(1));
|
|
12
71
|
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { print: { type: "boolean" } } });
|
|
13
72
|
if (positionals.length) throw new UsageError();
|
|
14
|
-
const
|
|
15
|
-
await start();
|
|
16
|
-
try {
|
|
17
|
-
const settings = JSON.parse(await readFile(path.join(alpHome(), "settings.json"), "utf8"));
|
|
18
|
-
if (settings?.web?.enabled === false) throw new Error('ALP web app is disabled by settings ("web.enabled": false).');
|
|
19
|
-
} catch (error) {
|
|
20
|
-
if (/disabled by settings/.test(error?.message ?? "")) throw error;
|
|
21
|
-
}
|
|
22
|
-
await startIntegration();
|
|
73
|
+
const entry = alpWebEntry();
|
|
23
74
|
const home = alpHome();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
info = await readFile(file, "utf8").then(JSON.parse, () => void 0);
|
|
28
|
-
let alive = false;
|
|
29
|
-
try {
|
|
30
|
-
if (info?.pid) {
|
|
31
|
-
process.kill(info.pid, 0);
|
|
32
|
-
alive = true;
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
alive = false;
|
|
36
|
-
}
|
|
37
|
-
let integrationPid = void 0;
|
|
38
|
-
try {
|
|
39
|
-
integrationPid = JSON.parse(await readFile(path.join(home, "integrations", "web.lock"), "utf8"))?.pid;
|
|
40
|
-
} catch {
|
|
41
|
-
}
|
|
42
|
-
if (info?.token && info?.url && alive && info.pid !== lock?.pid && (!integrationPid || info.pid === integrationPid)) break;
|
|
43
|
-
info = void 0;
|
|
44
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
45
|
-
}
|
|
46
|
-
if (!info) throw new Error(lockAlive(lock) ? 'alpd serves no web app: it is older than this alp (alp daemon restart), or "web": { "enabled": false } is set in ~/.alp/settings.json' : "alpd started without its web app; see ~/.alp/logs/alpd.log");
|
|
47
|
-
const url = `${info.url}#token=${info.token}`;
|
|
75
|
+
if (await webDisabled(home)) throw new Error(DISABLED);
|
|
76
|
+
await start();
|
|
77
|
+
const url = await serveWeb(home, entry);
|
|
48
78
|
console.log(url);
|
|
49
79
|
if (values.print) return;
|
|
50
80
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
51
81
|
spawn(opener, [url], { stdio: "ignore", detached: true }).on("error", () => console.error("Open the address above in your browser")).unref();
|
|
52
82
|
}
|
|
83
|
+
async function rotateCommand(args) {
|
|
84
|
+
if (args.length) throw new UsageError();
|
|
85
|
+
const entry = alpWebEntry();
|
|
86
|
+
const home = alpHome();
|
|
87
|
+
await start();
|
|
88
|
+
const project = await projectArgs();
|
|
89
|
+
for (let attempt = 1; ; attempt++) {
|
|
90
|
+
await stopWebService(home);
|
|
91
|
+
const rotated = await new Promise((resolve) => {
|
|
92
|
+
execFile(
|
|
93
|
+
process.execPath,
|
|
94
|
+
[entry, "--rotate", ...project],
|
|
95
|
+
{ env: { ...process.env, ALP_HOME: home }, encoding: "utf8", timeout: 6e4, killSignal: "SIGTERM" },
|
|
96
|
+
(error, stdout, stderr) => resolve({ code: error ? typeof error.code === "number" ? error.code : 1 : 0, output: `${stderr}${stdout}`.trim() })
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
if (rotated.code === 0) break;
|
|
100
|
+
if (rotated.code !== 3 || attempt >= 3) throw new Error(`alp web could not rotate its token${rotated.output ? `:
|
|
101
|
+
${rotated.output.slice(-2e3)}` : ""}`);
|
|
102
|
+
}
|
|
103
|
+
if (await webDisabled(home)) {
|
|
104
|
+
console.log(`alp web has a new token; the old one no longer works. ${DISABLED.replace(/\.$/, "")}, so it was not started.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.log(await serveWeb(home, entry));
|
|
108
|
+
}
|
|
53
109
|
async function requestPairing(client, params, attempts = 50) {
|
|
54
110
|
let last;
|
|
55
111
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
@@ -66,7 +122,8 @@ async function pairCommand(args) {
|
|
|
66
122
|
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { on: { type: "boolean" }, off: { type: "boolean" } } });
|
|
67
123
|
if (positionals.length || values.on && values.off) throw new UsageError();
|
|
68
124
|
await start();
|
|
69
|
-
|
|
125
|
+
const home = alpHome();
|
|
126
|
+
await startPaseoIntegration(home);
|
|
70
127
|
const client = await running();
|
|
71
128
|
try {
|
|
72
129
|
const pairing = await requestPairing(client, values.on ? { enable: true } : values.off ? { enable: false } : {});
|
|
@@ -75,9 +132,11 @@ async function pairCommand(args) {
|
|
|
75
132
|
return;
|
|
76
133
|
}
|
|
77
134
|
console.log(`Relay ${pairing.endpoint}: ${pairing.connected ? "connected" : "connecting"}`);
|
|
78
|
-
console.log(`Open this link on the device
|
|
135
|
+
console.log(`Open this link on the device that runs Paseo's app:
|
|
79
136
|
${pairing.url}`);
|
|
80
|
-
|
|
137
|
+
const password = await readFile(path.join(home, `${PASEO}.json`), "utf8").then((text) => JSON.parse(text)?.token).catch(() => void 0);
|
|
138
|
+
if (typeof password === "string" && password) console.log(`When Paseo's app asks for a password, give it: ${password}`);
|
|
139
|
+
else console.log(`When Paseo's app asks for a password, give it the token in ${path.join(home, `${PASEO}.json`)}.`);
|
|
81
140
|
} finally {
|
|
82
141
|
client.close();
|
|
83
142
|
}
|
|
@@ -92,5 +151,6 @@ export {
|
|
|
92
151
|
acpCommand,
|
|
93
152
|
pairCommand,
|
|
94
153
|
requestPairing,
|
|
154
|
+
startPaseoIntegration,
|
|
95
155
|
webCommand
|
|
96
156
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// packages/cli/src/schedules.js
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { running } from "./daemon.js";
|
|
5
|
+
import { UsageError } from "./usage.js";
|
|
6
|
+
async function scheduleCommand(args) {
|
|
7
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
|
|
8
|
+
project: { type: "string" },
|
|
9
|
+
session: { type: "string" },
|
|
10
|
+
at: { type: "string" },
|
|
11
|
+
prompt: { type: "string" },
|
|
12
|
+
status: { type: "string" },
|
|
13
|
+
json: { type: "boolean" }
|
|
14
|
+
} });
|
|
15
|
+
const [action, id, ...extra] = positionals;
|
|
16
|
+
if (!["create", "list", "show", "cancel"].includes(action) || extra.length) throw new UsageError();
|
|
17
|
+
if (action === "show" || action === "cancel" ? !id : !!id) throw new UsageError();
|
|
18
|
+
if (action !== "create" && (values.session || values.at || values.prompt)) throw new UsageError();
|
|
19
|
+
if (action !== "list" && values.status) throw new UsageError();
|
|
20
|
+
if (action === "create" && (!values.session || !values.prompt?.trim() || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(values.at ?? "") || !Number.isFinite(Date.parse(values.at)))) throw new Error("create needs --session, --prompt and --at ISO timestamp with Z or a UTC offset (for example 2026-10-20T16:00:00+07:00)");
|
|
21
|
+
const projectRoot = path.resolve(values.project ?? process.cwd());
|
|
22
|
+
const params = action === "create" ? { projectRoot, sessionId: values.session, dueAt: values.at, prompt: values.prompt } : { projectRoot, ...id ? { id } : {}, ...values.status ? { status: values.status } : {} };
|
|
23
|
+
const client = await running();
|
|
24
|
+
try {
|
|
25
|
+
const result = await client.request(`schedule.${action === "show" ? "get" : action}`, params);
|
|
26
|
+
if (values.json) {
|
|
27
|
+
console.log(JSON.stringify(result));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const jobs = result.jobs ?? [result.job];
|
|
31
|
+
if (!jobs.length) console.log("No scheduled continuations");
|
|
32
|
+
for (const job of jobs) {
|
|
33
|
+
console.log(`${job.id} ${job.status} ${job.dueAt} session ${job.sessionId}`);
|
|
34
|
+
console.log(` ${job.prompt}`);
|
|
35
|
+
if (job.attemptedAt) console.log(` attempted ${job.attemptedAt}; attempts ${job.attempts}; late ${job.lateByMs ?? 0} ms`);
|
|
36
|
+
for (const field of ["deliveredAt", "failedAt", "cancelledAt"]) if (job[field]) console.log(` ${field}: ${job[field]}`);
|
|
37
|
+
if (job.lastError) console.log(` ${job.lastError.code ?? "error"}: ${job.lastError.message}`);
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
client.close();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
scheduleCommand
|
|
45
|
+
};
|
|
@@ -7,9 +7,11 @@ export function answerer(client: any): {
|
|
|
7
7
|
close(): void;
|
|
8
8
|
};
|
|
9
9
|
/** Streams a tree until its root is idle; the first Ctrl-C interrupts it, the second detaches. */
|
|
10
|
-
export function follow(client: any, rootId: any, print: any, { untilIdle, interactive }: {
|
|
10
|
+
export function follow(client: any, rootId: any, print: any, { untilIdle, interactive, targetOnly, promptId }: {
|
|
11
11
|
untilIdle: any;
|
|
12
12
|
interactive?: boolean | undefined;
|
|
13
|
+
targetOnly?: boolean | undefined;
|
|
14
|
+
promptId: any;
|
|
13
15
|
}): Promise<any>;
|
|
14
16
|
export function run(args: any): Promise<void>;
|
|
15
17
|
export function ps(args: any): Promise<void>;
|
|
@@ -121,7 +121,7 @@ function answerer(client) {
|
|
|
121
121
|
}
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
|
-
function follow(client, rootId, print, { untilIdle, interactive = false }) {
|
|
124
|
+
function follow(client, rootId, print, { untilIdle, interactive = false, targetOnly = false, promptId }) {
|
|
125
125
|
const answering = interactive ? answerer(client) : void 0;
|
|
126
126
|
const done = new Promise((resolve, reject) => {
|
|
127
127
|
let failed = false;
|
|
@@ -144,10 +144,13 @@ function follow(client, rootId, print, { untilIdle, interactive = false }) {
|
|
|
144
144
|
process.on("SIGINT", onSignal);
|
|
145
145
|
client.onClose((error) => {
|
|
146
146
|
process.off("SIGINT", onSignal);
|
|
147
|
+
answering?.close();
|
|
147
148
|
reject(error);
|
|
148
149
|
});
|
|
149
150
|
let rootEnded = false;
|
|
150
151
|
client.onEvent((envelope) => {
|
|
152
|
+
if (targetOnly && envelope.sessionId !== rootId) return;
|
|
153
|
+
if (promptId && envelope.event.type === "prompt.failed" && envelope.event.clientMessageId !== promptId) return;
|
|
151
154
|
print(envelope);
|
|
152
155
|
answering?.accept(envelope.event);
|
|
153
156
|
const { sessionId, event } = envelope;
|
|
@@ -234,28 +237,36 @@ async function attach(args) {
|
|
|
234
237
|
}
|
|
235
238
|
}
|
|
236
239
|
async function send(args) {
|
|
237
|
-
const { values, positionals: [sessionId, ...words] } = parseArgs({ args, allowPositionals: true, options: { json: { type: "boolean" } } });
|
|
240
|
+
const { values, positionals: [sessionId, ...words] } = parseArgs({ args, allowPositionals: true, options: { json: { type: "boolean" }, prompt: { type: "boolean" } } });
|
|
238
241
|
const text = words.join(" ").trim();
|
|
239
242
|
if (!sessionId || !text) throw new UsageError();
|
|
240
243
|
const client = await connect(await start(), { name: "alp-cli", version: "1" });
|
|
244
|
+
let subscriptionId;
|
|
241
245
|
try {
|
|
242
246
|
const { session } = await client.request("session.get", { sessionId });
|
|
243
|
-
if (session.parentId) {
|
|
247
|
+
if (session.parentId && !values.prompt) {
|
|
244
248
|
await client.request("session.message", { sessionId, text });
|
|
245
249
|
if (!values.json) console.error(`Sent to ${session.agent} as mail from the user`);
|
|
246
250
|
return;
|
|
247
251
|
}
|
|
248
|
-
const done = follow(client, sessionId, printer(values.json), { untilIdle: true, interactive: !values.json && process.stdin.isTTY });
|
|
249
252
|
const live = session.status === "idle" || session.status === "running";
|
|
250
|
-
if (live)
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
if (values.prompt && !live) throw new Error("Explicit prompts require a live session; use default alp send to reopen a root");
|
|
254
|
+
const clientMessageId = randomUUID();
|
|
255
|
+
const done = follow(client, sessionId, printer(values.json), { untilIdle: true, interactive: !values.json && process.stdin.isTTY, targetOnly: !!session.parentId, promptId: clientMessageId });
|
|
256
|
+
if (live) {
|
|
257
|
+
const attached = await client.request("session.attach", { sessionId, replay: false });
|
|
258
|
+
subscriptionId = attached.session.id;
|
|
259
|
+
} else {
|
|
260
|
+
await client.request("session.create", { sessionId, spec: { cwd: session.projectRoot } });
|
|
261
|
+
subscriptionId = sessionId;
|
|
262
|
+
}
|
|
263
|
+
const steer = !values.prompt && live && !!session.activeTurnId;
|
|
264
|
+
await client.request("session.prompt", { sessionId, clientMessageId, delivery: steer ? "steer" : "auto", content: [{ type: "text", text }] });
|
|
265
|
+
if (!values.json) console.error(values.prompt ? "Submitted an explicit prompt" : steer ? "Steered the running turn" : live ? "Started a turn" : "Resumed the session");
|
|
255
266
|
if (!await done) process.exitCode = 1;
|
|
256
|
-
await client.request("session.release", { sessionId }).catch(() => {
|
|
257
|
-
});
|
|
258
267
|
} finally {
|
|
268
|
+
if (subscriptionId) await client.request("session.release", { sessionId: subscriptionId }).catch(() => {
|
|
269
|
+
});
|
|
259
270
|
client.close();
|
|
260
271
|
}
|
|
261
272
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function terminalCommand(args: any, { home, out }?: {
|
|
2
|
+
home?: string | undefined;
|
|
3
|
+
out?: {
|
|
4
|
+
(...data: any[]): void;
|
|
5
|
+
(message?: any, ...optionalParams: any[]): void;
|
|
6
|
+
} | undefined;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
export function TERMINAL_WARNING(user?: string): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// packages/cli/src/terminal.js
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { chmod, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { alpHome } from "@anhlp/alp-client/index";
|
|
7
|
+
import { UsageError } from "./usage.js";
|
|
8
|
+
var TERMINAL_WARNING = (user = os.userInfo().username) => [
|
|
9
|
+
`A remote terminal runs a real shell on this computer as ${user}, with that account's full access:`,
|
|
10
|
+
"every file it can read or write, any program, and the network.",
|
|
11
|
+
"A workspace only picks the directory the shell starts in. It is NOT a sandbox.",
|
|
12
|
+
"Only gateway credentials that hold the terminal.exec permission can open one (never a default);",
|
|
13
|
+
"Paseo app devices paired with this host only after alp terminal allow --paseo."
|
|
14
|
+
].join("\n");
|
|
15
|
+
var file = (home) => path.join(home, "terminal.json");
|
|
16
|
+
async function read(home) {
|
|
17
|
+
const info = await lstat(file(home)).catch(() => void 0);
|
|
18
|
+
if (!info) return { exists: false, value: void 0, private: true };
|
|
19
|
+
const own = !process.getuid || info.uid === process.getuid();
|
|
20
|
+
const value = info.isFile() ? await readFile(file(home), "utf8").then(JSON.parse, () => void 0) : void 0;
|
|
21
|
+
return { exists: true, value, private: info.isFile() && own && !(info.mode & 63) };
|
|
22
|
+
}
|
|
23
|
+
async function write(home, value) {
|
|
24
|
+
await mkdir(home, { recursive: true, mode: 448 });
|
|
25
|
+
const temporary = `${file(home)}.${randomUUID().slice(0, 8)}.tmp`;
|
|
26
|
+
await writeFile(temporary, JSON.stringify(value, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
27
|
+
await chmod(temporary, 384);
|
|
28
|
+
await rename(temporary, file(home)).catch(async (error) => {
|
|
29
|
+
await rm(temporary, { force: true });
|
|
30
|
+
throw error;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function terminalCommand(args, { home = alpHome(), out = console.log } = {}) {
|
|
34
|
+
const [action, ...rest] = args;
|
|
35
|
+
const json = rest.includes("--json");
|
|
36
|
+
const current = await read(home);
|
|
37
|
+
const limits = current.value && typeof current.value.limits === "object" && current.value.limits ? { limits: current.value.limits } : {};
|
|
38
|
+
const paseo = rest.includes("--paseo");
|
|
39
|
+
const paseoBefore = current.private && current.value?.allowPaseoTerminal === true;
|
|
40
|
+
if (action === "allow") {
|
|
41
|
+
if (!rest.includes("--yes")) {
|
|
42
|
+
out(`${TERMINAL_WARNING()}
|
|
43
|
+
|
|
44
|
+
Run again with --yes to allow remote terminals on this host.`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const allowPaseoTerminal = paseo || paseoBefore;
|
|
49
|
+
await write(home, { version: 1, allowRemoteTerminal: true, ...allowPaseoTerminal ? { allowPaseoTerminal: true } : {}, ...limits });
|
|
50
|
+
out(json ? JSON.stringify({ allowRemoteTerminal: true, allowPaseoTerminal }) : `${TERMINAL_WARNING()}
|
|
51
|
+
|
|
52
|
+
Remote terminals are allowed on this host (${file(home)})${allowPaseoTerminal ? ", also for every Paseo app device paired with it" : ""}. alp terminal deny turns them off and ends every open one.`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (action === "deny") {
|
|
56
|
+
if (paseo) {
|
|
57
|
+
const allowed = current.private && current.value?.allowRemoteTerminal === true;
|
|
58
|
+
await write(home, { version: 1, allowRemoteTerminal: allowed, ...limits });
|
|
59
|
+
out(json ? JSON.stringify({ allowRemoteTerminal: allowed, allowPaseoTerminal: false }) : "Paseo app devices can no longer open terminals on this host; the bridge ends the terminals it holds.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await write(home, { version: 1, allowRemoteTerminal: false, ...limits });
|
|
63
|
+
out(json ? JSON.stringify({ allowRemoteTerminal: false, allowPaseoTerminal: false }) : "Remote terminals are off on this host; alpd ends every open terminal within 5 seconds.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (action === "status") {
|
|
67
|
+
const allowed = current.private && current.value?.version === 1 && current.value.allowRemoteTerminal === true;
|
|
68
|
+
const allowPaseoTerminal = allowed && current.value.allowPaseoTerminal === true;
|
|
69
|
+
const problem = current.exists && !current.private ? `${file(home)} is not a private file of this user, so terminals stay off` : void 0;
|
|
70
|
+
if (json) out(JSON.stringify({ allowRemoteTerminal: allowed, allowPaseoTerminal, ...limits, ...problem ? { problem } : {} }));
|
|
71
|
+
else out([`Remote terminals: ${allowed ? "allowed" : "off"}${allowPaseoTerminal ? " (also for paired Paseo app devices)" : ""}`, ...problem ? [problem] : [], ...allowed ? ["", TERMINAL_WARNING()] : []].join("\n"));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
throw new UsageError();
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
TERMINAL_WARNING,
|
|
78
|
+
terminalCommand
|
|
79
|
+
};
|