directionallylocal 0.8.0 → 0.8.1
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 +1 -1
- package/dist/laptop/session.js +7 -2
- package/dist/laptop/settings.js +9 -2
- package/dist/laptop/setup.js +43 -5
- package/dist/local/check.js +17 -3
- package/dist/local/keys.js +4 -3
- package/dist/local/setup.js +13 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ dl
|
|
|
11
11
|
|
|
12
12
|
`dl` asks where your agents should run:
|
|
13
13
|
|
|
14
|
-
- **On this computer** — start now, nothing to rent, pauses when this computer sleeps.
|
|
14
|
+
- **On this computer** — start now, nothing to rent, pauses when this computer sleeps. macOS and Linux; not offered on Windows yet (connect a server, or run `dl` inside WSL 2, which takes the Linux path).
|
|
15
15
|
- **On my own server** — keeps going when your laptop is closed; needs SSH access.
|
|
16
16
|
|
|
17
17
|
For a server, `dl` lists the hosts in `~/.ssh/config` (or takes `user@host[:port]`), checks it over SSH, installs itself there, and pairs this laptop. Nothing is exposed on the server: `dl` reaches it through an SSH tunnel.
|
package/dist/laptop/session.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function projectSession(input, io) {
|
|
|
55
55
|
note(describeError(error));
|
|
56
56
|
return undefined;
|
|
57
57
|
}
|
|
58
|
-
const { remote } = target;
|
|
58
|
+
const { remote, reason } = target;
|
|
59
59
|
const key = { place: remote.name, repository, root };
|
|
60
60
|
const file = input.sessionsFile ?? sessionsPath();
|
|
61
61
|
const autopilot = await (input.autopilot ?? findAutopilot)();
|
|
@@ -142,7 +142,12 @@ export async function projectSession(input, io) {
|
|
|
142
142
|
// `dl new`: the old conversation keeps running on the place (dl runs lists it).
|
|
143
143
|
if (input.fresh)
|
|
144
144
|
await forgetSession(key, file);
|
|
145
|
-
|
|
145
|
+
// The place is on the Connecting line already; with more than one set up, also why this
|
|
146
|
+
// one and how to pick another, as `dl run` does (a place named with --server needs neither).
|
|
147
|
+
const chosen = reason === "default" || reason === "last used";
|
|
148
|
+
io.err(`\n ${c.cyan("·")} Connecting to ${c.bold(remote.name)}${chosen ? ` ${c.dim(`(${reason})`)}` : ""} · ${repository} … ${c.dim(`(${detail})`)}\n`);
|
|
149
|
+
if (chosen)
|
|
150
|
+
io.err(` ${c.dim("dl use NAME to change where dl runs")}\n`);
|
|
146
151
|
// The token goes in the environment only: argv is visible to every user on the machine.
|
|
147
152
|
const env = { ...process.env, AUTOPILOT_REMOTE_URL: client.baseUrl, AUTOPILOT_REMOTE_TOKEN: remote.token };
|
|
148
153
|
let saved = remembered?.conversationId;
|
package/dist/laptop/settings.js
CHANGED
|
@@ -37,7 +37,10 @@ export async function loadSettings(path = settingsPath()) {
|
|
|
37
37
|
}
|
|
38
38
|
try {
|
|
39
39
|
const parsed = JSON.parse(raw);
|
|
40
|
-
return
|
|
40
|
+
return {
|
|
41
|
+
...(typeof parsed.model === "string" && parsed.model.trim() ? { model: parsed.model.trim() } : {}),
|
|
42
|
+
...(parsed.askedLocalDefault === true ? { askedLocalDefault: true } : {}),
|
|
43
|
+
};
|
|
41
44
|
}
|
|
42
45
|
catch {
|
|
43
46
|
// A damaged file only loses the saved model; the next save rewrites it.
|
|
@@ -45,7 +48,11 @@ export async function loadSettings(path = settingsPath()) {
|
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
export async function saveModel(model, path = settingsPath()) {
|
|
48
|
-
|
|
51
|
+
await saveSettings({ model }, path);
|
|
52
|
+
}
|
|
53
|
+
/** Merges into dl's settings, keeping what's already there. */
|
|
54
|
+
export async function saveSettings(changes, path = settingsPath()) {
|
|
55
|
+
const settings = { ...(await loadSettings(path)), ...changes };
|
|
49
56
|
await mkdir(dirname(path), { recursive: true });
|
|
50
57
|
const temp = `${path}.${process.pid}.tmp`;
|
|
51
58
|
await writeFile(temp, `${JSON.stringify({ version: 1, ...settings }, null, 2)}\n`, { mode: 0o600 });
|
package/dist/laptop/setup.js
CHANGED
|
@@ -5,11 +5,12 @@ import { join, resolve } from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { PromptCancelled, TtyPrompter } from "../prompt.js";
|
|
7
7
|
import { palette, PlainUi, TerminalUi } from "../ui.js";
|
|
8
|
-
import {
|
|
8
|
+
import { localUnsupported } from "../local/check.js";
|
|
9
|
+
import { loadRemotes, loadRemotesFile, normalizeServerUrl, REMOTE_NAME_RE, remotesPath, resolveTarget, saveRemote, setDefaultRemote } from "./config.js";
|
|
9
10
|
import { confirm, gatherKeys } from "./credentials.js";
|
|
10
11
|
import { bootstrap, INSTALL_URL, installOnServer, pairLaptop, serverNeeds } from "./remote-install.js";
|
|
11
12
|
import { checkServer, describeHost, discoverKeys, hostOf, parseSshConfig, parseTarget, realSshExec, SshAuthError } from "./ssh.js";
|
|
12
|
-
import { askModel, DEFAULT_MODEL, loadSettings, modelPrices, readAutopilotModel, saveModel } from "./settings.js";
|
|
13
|
+
import { askModel, DEFAULT_MODEL, loadSettings, modelPrices, readAutopilotModel, saveModel, saveSettings } from "./settings.js";
|
|
13
14
|
import { openRemote, withConnections } from "./tunnel.js";
|
|
14
15
|
const RUN_EXAMPLE = 'dl run "fix the flaky checkout test"';
|
|
15
16
|
const PLACES = [
|
|
@@ -20,6 +21,13 @@ export async function setupWizard(deps) {
|
|
|
20
21
|
const c = palette(deps.color);
|
|
21
22
|
deps.write(`\n ${c.bold("Directionally Local")}\n Background coding agents. Where should they run?\n\n`);
|
|
22
23
|
const ui = deps.newUi();
|
|
24
|
+
// Where this computer can't run agents (Windows), the list would hold one real choice: say why, and go on.
|
|
25
|
+
const unsupported = localUnsupported(deps.platform ?? process.platform);
|
|
26
|
+
if (unsupported) {
|
|
27
|
+
ui.line("info", "On this computer", unsupported);
|
|
28
|
+
ui.answered("Where", "On my own server");
|
|
29
|
+
return setUpServer(deps);
|
|
30
|
+
}
|
|
23
31
|
let where = (await deps.prompter.select(ui.prompt("Where"), PLACES, 0)).value;
|
|
24
32
|
ui.answered("Where", where === "local" ? "On this computer" : "On my own server");
|
|
25
33
|
if (where === "local" && !deps.localSetup) {
|
|
@@ -39,8 +47,7 @@ export async function setupWizard(deps) {
|
|
|
39
47
|
const remotesFile = deps.remotesFile ?? remotesPath();
|
|
40
48
|
const isNew = !(await loadRemotes(remotesFile)).some((entry) => entry.name === remote.name);
|
|
41
49
|
await saveRemote(remote, remotesFile);
|
|
42
|
-
|
|
43
|
-
await chooseDefault(deps, remote, remotesFile);
|
|
50
|
+
await offerThisComputer(deps, remote, remotesFile, isNew);
|
|
44
51
|
await chooseModel(deps);
|
|
45
52
|
finishScreen(deps.newUi(), capacity);
|
|
46
53
|
return remote;
|
|
@@ -142,7 +149,7 @@ async function chooseServer(deps, hosts) {
|
|
|
142
149
|
}
|
|
143
150
|
/**
|
|
144
151
|
* With two or more places set up, where `dl run` sends tasks unless told otherwise. Asked
|
|
145
|
-
* only when setup adds a
|
|
152
|
+
* only when setup adds a server, with the new one preselected (this computer: offerThisComputer); the first place becomes the
|
|
146
153
|
* default by itself (saveRemote).
|
|
147
154
|
*/
|
|
148
155
|
async function chooseDefault(deps, added, remotesFile) {
|
|
@@ -158,6 +165,36 @@ async function chooseDefault(deps, added, remotesFile) {
|
|
|
158
165
|
ui.answered("Default", value);
|
|
159
166
|
ui.finish();
|
|
160
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* After "on this computer", when commands would still go somewhere else (a server set up
|
|
170
|
+
* first stays the default, saveRemote): asks once, at a terminal, whether this computer
|
|
171
|
+
* should be the default instead. Asked again only when this computer is newly added.
|
|
172
|
+
* Otherwise (no terminal, already asked, or "no") it says where things go and how to change it.
|
|
173
|
+
*/
|
|
174
|
+
async function offerThisComputer(deps, local, remotesFile, isNew) {
|
|
175
|
+
const { remotes } = await loadRemotesFile(remotesFile);
|
|
176
|
+
if (remotes.length < 2)
|
|
177
|
+
return;
|
|
178
|
+
const current = await resolveTarget(undefined, remotesFile).then((target) => target.remote.name, () => undefined);
|
|
179
|
+
if (current === local.name)
|
|
180
|
+
return;
|
|
181
|
+
const hint = `dl use ${local.name}`;
|
|
182
|
+
const ui = deps.newUi();
|
|
183
|
+
deps.write("\n");
|
|
184
|
+
if (deps.interactive !== false && (isNew || !(await loadSettings(deps.settingsFile)).askedLocalDefault)) {
|
|
185
|
+
const question = current ? `Make this computer the default? Tasks and sessions go to ${current} now` : "Make this computer the default for tasks and sessions?";
|
|
186
|
+
const yes = await confirm(ui, deps.prompter, question);
|
|
187
|
+
await saveSettings({ askedLocalDefault: true }, deps.settingsFile);
|
|
188
|
+
if (yes) {
|
|
189
|
+
await setDefaultRemote(local.name, remotesFile);
|
|
190
|
+
ui.answered("Default", "this computer");
|
|
191
|
+
ui.finish();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
ui.line("info", "Default", current ? `still ${current} · ${hint} to run here instead` : `none yet · ${hint} to run here`);
|
|
196
|
+
ui.finish();
|
|
197
|
+
}
|
|
161
198
|
/**
|
|
162
199
|
* Which model tasks and sessions use, asked once: setup skips it when dl already has one
|
|
163
200
|
* (dl model changes it). Autopilot's own default is preselected for people who have one.
|
|
@@ -335,6 +372,7 @@ export async function setup(options) {
|
|
|
335
372
|
installUrl: process.env.DL_INSTALL_URL || INSTALL_URL,
|
|
336
373
|
...(options.localSetup ? { localSetup: options.localSetup } : {}),
|
|
337
374
|
...(identityFile ? { identityFile } : {}),
|
|
375
|
+
interactive: terminal && process.stdin.isTTY === true,
|
|
338
376
|
});
|
|
339
377
|
}
|
|
340
378
|
catch (error) {
|
package/dist/local/check.js
CHANGED
|
@@ -41,9 +41,9 @@ export async function checkComputer(system, options) {
|
|
|
41
41
|
/** What stops setup before it changes anything, each with how to fix it. */
|
|
42
42
|
export function computerProblems(facts) {
|
|
43
43
|
const problems = [];
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const unsupported = localUnsupported(facts.platform);
|
|
45
|
+
if (unsupported)
|
|
46
|
+
return [unsupported];
|
|
47
47
|
if (facts.platform === "linux" && !facts.systemdUser) {
|
|
48
48
|
problems.push("A systemd user session is needed to keep the agents running (systemctl --user). Log in to a desktop session, or set up a server instead.");
|
|
49
49
|
}
|
|
@@ -79,6 +79,20 @@ function dockerFix(facts) {
|
|
|
79
79
|
export function localCapacity(docker) {
|
|
80
80
|
return agentCapacity({ memoryMb: docker.memoryMb, cpus: docker.cpus }, LOCAL_CELL_MEMORY_MB);
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Why agents can't run on this computer's platform, or undefined on macOS and Linux. The
|
|
84
|
+
* local setup is POSIX throughout (sh, launchd or systemd --user, unix sockets), so other
|
|
85
|
+
* platforms are refused before anything runs. WSL reports "linux" and takes the Linux path.
|
|
86
|
+
*/
|
|
87
|
+
export function localUnsupported(platform) {
|
|
88
|
+
if (supportedPlatform(platform))
|
|
89
|
+
return undefined;
|
|
90
|
+
if (platform === "win32") {
|
|
91
|
+
return "Running on this computer isn't supported on Windows yet; connect a server instead (dl setup, then \"On my own server\"). "
|
|
92
|
+
+ "Inside WSL 2, dl runs as on Linux (it needs systemd and Docker there).";
|
|
93
|
+
}
|
|
94
|
+
return `Running agents on this computer works on macOS and Linux, not ${platform}. Connect a server instead: dl setup, then "On my own server".`;
|
|
95
|
+
}
|
|
82
96
|
export function supportedPlatform(platform) {
|
|
83
97
|
return platform === "darwin" || platform === "linux" ? platform : undefined;
|
|
84
98
|
}
|
package/dist/local/keys.js
CHANGED
|
@@ -8,7 +8,7 @@ import { homedir } from "node:os";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { checkKey, githubListing, hotcellKeyListed, KeyRejectedError } from "../keys.js";
|
|
10
10
|
import { RealSystem } from "../system.js";
|
|
11
|
-
import { supportedPlatform } from "./check.js";
|
|
11
|
+
import { localUnsupported, supportedPlatform } from "./check.js";
|
|
12
12
|
import { LOCAL_SERVICES, localGithubEnv, localHotcellBin, localPaths, parseLocalEnvFile, serviceFile } from "./layout.js";
|
|
13
13
|
import { httpStatus } from "./probes.js";
|
|
14
14
|
import { userId } from "./services.js";
|
|
@@ -16,9 +16,10 @@ import { hotcellCliEnv } from "./setup.js";
|
|
|
16
16
|
import { readLocalState } from "./state.js";
|
|
17
17
|
async function installed(options) {
|
|
18
18
|
const home = options.home ?? homedir();
|
|
19
|
-
const
|
|
19
|
+
const raw = options.platform ?? process.platform;
|
|
20
|
+
const platform = supportedPlatform(raw);
|
|
20
21
|
if (!platform)
|
|
21
|
-
throw new Error(
|
|
22
|
+
throw new Error(localUnsupported(raw));
|
|
22
23
|
const system = options.system ?? new RealSystem(() => { });
|
|
23
24
|
const paths = localPaths(home, platform);
|
|
24
25
|
const state = await readLocalState(system, paths);
|
package/dist/local/setup.js
CHANGED
|
@@ -26,7 +26,7 @@ import { join } from "node:path";
|
|
|
26
26
|
import { cellDockerfile, cellImage } from "../layout.js";
|
|
27
27
|
import { removeRemote } from "../laptop/config.js";
|
|
28
28
|
import { RealSystem } from "../system.js";
|
|
29
|
-
import { checkComputer, computerProblems, localCapacity, servicePath, supportedPlatform } from "./check.js";
|
|
29
|
+
import { checkComputer, computerProblems, localCapacity, localUnsupported, servicePath, supportedPlatform } from "./check.js";
|
|
30
30
|
import { LOCAL_SERVICES, PAIRED_NAME, PREFERRED_PORTS, autopilotXdg, localAsterConfig, localAutopilotBin, localAutopilotEnv, localGithubEnv, localHotcellBin, localHotcellEnv, localPaths, parseLocalEnvFile, serviceSpecs, } from "./layout.js";
|
|
31
31
|
import { fileMtime, modelPricesHealth, refreshModelPrices } from "../model-prices.js";
|
|
32
32
|
import { httpStatus, portFree } from "./probes.js";
|
|
@@ -37,10 +37,16 @@ const SETUP_LOG = "setup.log";
|
|
|
37
37
|
export function localSetupLog(home = homedir()) {
|
|
38
38
|
return join(home, ".directionallylocal", "logs", SETUP_LOG);
|
|
39
39
|
}
|
|
40
|
+
/** Stops on a platform the local setup can't run on (Windows), before any command runs. */
|
|
41
|
+
export function assertLocalSupported(platform = process.platform) {
|
|
42
|
+
const unsupported = localUnsupported(platform);
|
|
43
|
+
if (unsupported)
|
|
44
|
+
throw new Error(unsupported);
|
|
45
|
+
}
|
|
40
46
|
function context(options, logName) {
|
|
41
47
|
const home = options.home ?? homedir();
|
|
42
48
|
const rawPlatform = options.platform ?? process.platform;
|
|
43
|
-
//
|
|
49
|
+
// Callers refuse unsupported platforms first (assertLocalSupported); this only keeps types simple.
|
|
44
50
|
const platform = supportedPlatform(rawPlatform) ?? "linux";
|
|
45
51
|
return {
|
|
46
52
|
// Other tools' output goes to a log file (or nowhere), never the screen.
|
|
@@ -54,12 +60,15 @@ function context(options, logName) {
|
|
|
54
60
|
}
|
|
55
61
|
/** Checks this computer without changing anything, e.g. before the wizard asks for keys. */
|
|
56
62
|
export async function checkThisComputer(options = {}) {
|
|
63
|
+
assertLocalSupported(options.platform);
|
|
57
64
|
const ctx = context(options);
|
|
58
65
|
const facts = await checkComputer(ctx.system, checkOptions(ctx, options));
|
|
59
66
|
return { facts, problems: computerProblems(facts) };
|
|
60
67
|
}
|
|
61
68
|
export async function setUpOnThisComputer(input) {
|
|
62
69
|
const { ui } = input;
|
|
70
|
+
// Before the checklist: on Windows nothing here (sh, launchd/systemd, unix sockets) works.
|
|
71
|
+
assertLocalSupported(input.platform);
|
|
63
72
|
const ctx = context(input, SETUP_LOG);
|
|
64
73
|
const { system, paths, platform } = ctx;
|
|
65
74
|
const now = () => (input.now ?? (() => new Date()))().toISOString();
|
|
@@ -84,9 +93,6 @@ export async function setUpOnThisComputer(input) {
|
|
|
84
93
|
};
|
|
85
94
|
const { facts, uid } = await step("check", async () => {
|
|
86
95
|
let facts = await checkComputer(system, checkOptions(ctx, input));
|
|
87
|
-
const unsupported = computerProblems(facts);
|
|
88
|
-
if (ctx.rawPlatform !== "darwin" && ctx.rawPlatform !== "linux")
|
|
89
|
-
throw new Error(unsupported[0]);
|
|
90
96
|
const uid = await userId(system);
|
|
91
97
|
if (!facts.docker && facts.dockerApp && platform === "darwin") {
|
|
92
98
|
ui.start("check", `opening ${facts.dockerApp}`);
|
|
@@ -231,6 +237,7 @@ export async function setUpOnThisComputer(input) {
|
|
|
231
237
|
*/
|
|
232
238
|
export async function uninstallFromThisComputer(input) {
|
|
233
239
|
const { ui } = input;
|
|
240
|
+
assertLocalSupported(input.platform);
|
|
234
241
|
const ctx = context(input, "uninstall.log");
|
|
235
242
|
const { system, paths, platform } = ctx;
|
|
236
243
|
const state = await readLocalState(system, paths);
|
|
@@ -284,6 +291,7 @@ export async function uninstallFromThisComputer(input) {
|
|
|
284
291
|
}
|
|
285
292
|
/** `dl local status`: is it running, where, and how much fits. Read-only. */
|
|
286
293
|
export async function localStatus(options = {}) {
|
|
294
|
+
assertLocalSupported(options.platform);
|
|
287
295
|
const ctx = context(options);
|
|
288
296
|
const { system, paths, platform } = ctx;
|
|
289
297
|
const state = await readLocalState(system, paths);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "directionallylocal",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Directionally Local: background coding agents on your own server. One-line server install, and the dl command for your server and laptop.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://directionallylocal.com",
|