@devstationlabs/cli 0.1.1 → 0.1.3
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 +22 -5
- package/devstation.js +751 -120
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,16 +27,33 @@ Either way, check the install:
|
|
|
27
27
|
devstation doctor
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Set up a model
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
```sh
|
|
33
|
+
devstation login
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Pick a provider, paste a key (it is hidden as you type), choose a model. It is
|
|
37
|
+
stored in `~/.devstation/credentials.json`, readable only by you, so every new
|
|
38
|
+
terminal just works. Anthropic, OpenRouter, OpenAI, and any OpenAI-compatible
|
|
39
|
+
server — Ollama, LM Studio, Groq — are supported:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
devstation login openai # then give it http://localhost:11434/v1 for Ollama
|
|
43
|
+
devstation config # what it will use, and where each value came from
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Exporting `ANTHROPIC_API_KEY` or `OPENROUTER_API_KEY` still works, and is what a
|
|
47
|
+
server or CI job should do.
|
|
48
|
+
|
|
49
|
+
## Upgrade
|
|
33
50
|
|
|
34
51
|
```sh
|
|
35
|
-
|
|
36
|
-
export OPENROUTER_API_KEY=... # also works
|
|
52
|
+
devstation upgrade
|
|
37
53
|
```
|
|
38
54
|
|
|
39
|
-
|
|
55
|
+
It upgrades however it was installed: through npm for an npm install, or by
|
|
56
|
+
downloading and verifying the new binary for the standalone one.
|
|
40
57
|
|
|
41
58
|
## Use it
|
|
42
59
|
|
package/devstation.js
CHANGED
|
@@ -12504,12 +12504,13 @@ var init_sdk = __esm(() => {
|
|
|
12504
12504
|
|
|
12505
12505
|
// src/lib/agent/cli/index.ts
|
|
12506
12506
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
12507
|
+
import { Writable } from "stream";
|
|
12507
12508
|
import { resolve as resolve4 } from "path";
|
|
12508
|
-
import { existsSync as
|
|
12509
|
+
import { existsSync as existsSync12 } from "fs";
|
|
12509
12510
|
|
|
12510
12511
|
// src/lib/agent/cli/args.ts
|
|
12511
12512
|
var CLI_NAME = "devstation";
|
|
12512
|
-
var VERSION = "0.1.
|
|
12513
|
+
var VERSION = "0.1.3";
|
|
12513
12514
|
var COMMANDS = new Set([
|
|
12514
12515
|
"chat",
|
|
12515
12516
|
"run",
|
|
@@ -12525,6 +12526,9 @@ var COMMANDS = new Set([
|
|
|
12525
12526
|
"diff",
|
|
12526
12527
|
"tools",
|
|
12527
12528
|
"config",
|
|
12529
|
+
"login",
|
|
12530
|
+
"logout",
|
|
12531
|
+
"upgrade",
|
|
12528
12532
|
"doctor",
|
|
12529
12533
|
"version",
|
|
12530
12534
|
"help"
|
|
@@ -12537,6 +12541,9 @@ var OFFLINE_COMMANDS = new Set([
|
|
|
12537
12541
|
"diff",
|
|
12538
12542
|
"tools",
|
|
12539
12543
|
"config",
|
|
12544
|
+
"login",
|
|
12545
|
+
"logout",
|
|
12546
|
+
"upgrade",
|
|
12540
12547
|
"doctor",
|
|
12541
12548
|
"version",
|
|
12542
12549
|
"help",
|
|
@@ -12552,6 +12559,8 @@ function parseArgs(argv, cwd = process.cwd()) {
|
|
|
12552
12559
|
yes: false,
|
|
12553
12560
|
json: false,
|
|
12554
12561
|
sandbox: (process.env.DEVSTATION_SANDBOX ?? "").toLowerCase() !== "off",
|
|
12562
|
+
project: false,
|
|
12563
|
+
check: false,
|
|
12555
12564
|
root: cwd
|
|
12556
12565
|
};
|
|
12557
12566
|
const words = [];
|
|
@@ -12576,6 +12585,12 @@ function parseArgs(argv, cwd = process.cwd()) {
|
|
|
12576
12585
|
case "--no-sandbox":
|
|
12577
12586
|
parsed.sandbox = false;
|
|
12578
12587
|
break;
|
|
12588
|
+
case "--project":
|
|
12589
|
+
parsed.project = true;
|
|
12590
|
+
break;
|
|
12591
|
+
case "--check":
|
|
12592
|
+
parsed.check = true;
|
|
12593
|
+
break;
|
|
12579
12594
|
case "-h":
|
|
12580
12595
|
case "--help":
|
|
12581
12596
|
parsed.command = "help";
|
|
@@ -12664,7 +12679,13 @@ var HELP = `DevStation, the coding agent.
|
|
|
12664
12679
|
${CLI_NAME} memory show what it has been told about this project
|
|
12665
12680
|
${CLI_NAME} mcp the MCP servers configured here, and their tools
|
|
12666
12681
|
${CLI_NAME} tools list the tools it can use, and which ones ask first
|
|
12667
|
-
${CLI_NAME}
|
|
12682
|
+
${CLI_NAME} login [provider] store an API key and choose a model
|
|
12683
|
+
${CLI_NAME} logout [provider] remove stored API keys
|
|
12684
|
+
${CLI_NAME} upgrade [--check] update to the latest version (--check only reports)
|
|
12685
|
+
${CLI_NAME} config show the settings a run would use, and where each came from
|
|
12686
|
+
${CLI_NAME} config set <key> <value> [--project]
|
|
12687
|
+
set provider, model or baseUrl
|
|
12688
|
+
${CLI_NAME} config get|unset <key>, config path
|
|
12668
12689
|
${CLI_NAME} doctor check this machine is set up to run it
|
|
12669
12690
|
${CLI_NAME} version print the version
|
|
12670
12691
|
${CLI_NAME} help this
|
|
@@ -12681,12 +12702,15 @@ Options
|
|
|
12681
12702
|
-f, --follow keep watching (status only)
|
|
12682
12703
|
--json JSON from config, sessions, checkpoints and tools
|
|
12683
12704
|
--no-sandbox run commands on this machine instead of in a container
|
|
12705
|
+
--project with config set/unset: write this workspace's config, not the global one
|
|
12706
|
+
--check with upgrade: say whether a newer version exists, change nothing
|
|
12684
12707
|
-h, --help this
|
|
12685
12708
|
-v, --version the version
|
|
12686
12709
|
|
|
12687
12710
|
The repo command also needs GITHUB_TOKEN, with permission to push to that repository.
|
|
12688
12711
|
|
|
12689
|
-
Set ANTHROPIC_API_KEY
|
|
12712
|
+
Set up a model with \`${CLI_NAME} login\`, or export ANTHROPIC_API_KEY or OPENROUTER_API_KEY.
|
|
12713
|
+
Settings live in ~/.devstation/config.json, keys in ~/.devstation/credentials.json.
|
|
12690
12714
|
`;
|
|
12691
12715
|
var SESSION_HELP = ` /undo rewind the last checkpoint
|
|
12692
12716
|
/status what this session has done so far
|
|
@@ -12704,7 +12728,7 @@ var SESSION_HELP = ` /undo rewind the last checkpoint
|
|
|
12704
12728
|
`;
|
|
12705
12729
|
|
|
12706
12730
|
// src/lib/agent/cli/commands.ts
|
|
12707
|
-
import { accessSync, constants as constants2, existsSync as
|
|
12731
|
+
import { accessSync, constants as constants2, existsSync as existsSync10 } from "fs";
|
|
12708
12732
|
|
|
12709
12733
|
// src/lib/agent/git.ts
|
|
12710
12734
|
import { existsSync } from "fs";
|
|
@@ -17877,7 +17901,10 @@ class AnthropicProvider {
|
|
|
17877
17901
|
model;
|
|
17878
17902
|
client;
|
|
17879
17903
|
constructor(opts = {}) {
|
|
17880
|
-
this.client = new Anthropic(
|
|
17904
|
+
this.client = new Anthropic({
|
|
17905
|
+
...opts.apiKey ? { apiKey: opts.apiKey } : {},
|
|
17906
|
+
...opts.baseUrl ? { baseURL: opts.baseUrl } : {}
|
|
17907
|
+
});
|
|
17881
17908
|
this.model = opts.model || DEFAULT_MODEL;
|
|
17882
17909
|
}
|
|
17883
17910
|
async generate(input) {
|
|
@@ -17929,7 +17956,7 @@ class AnthropicProvider {
|
|
|
17929
17956
|
}
|
|
17930
17957
|
|
|
17931
17958
|
// src/lib/agent/providers/openrouter.ts
|
|
17932
|
-
var
|
|
17959
|
+
var OPENROUTER_BASE = "https://openrouter.ai/api/v1";
|
|
17933
17960
|
function stopReasonOf2(raw) {
|
|
17934
17961
|
switch (raw) {
|
|
17935
17962
|
case "stop":
|
|
@@ -17980,20 +18007,24 @@ function parseArguments(raw) {
|
|
|
17980
18007
|
|
|
17981
18008
|
class OpenRouterProvider {
|
|
17982
18009
|
apiKey;
|
|
17983
|
-
name
|
|
18010
|
+
name;
|
|
17984
18011
|
model;
|
|
17985
|
-
|
|
18012
|
+
endpoint;
|
|
18013
|
+
constructor(apiKey, model, opts = {}) {
|
|
17986
18014
|
this.apiKey = apiKey;
|
|
17987
|
-
this.
|
|
18015
|
+
this.name = opts.name ?? "openrouter";
|
|
18016
|
+
const base = (opts.baseUrl || OPENROUTER_BASE).replace(/\/+$/, "");
|
|
18017
|
+
this.endpoint = `${base}/chat/completions`;
|
|
18018
|
+
this.model = model || process.env.AI_MODEL || (this.name === "openrouter" ? "anthropic/claude-sonnet-5" : "");
|
|
17988
18019
|
}
|
|
17989
18020
|
async generate(input) {
|
|
17990
|
-
const
|
|
18021
|
+
const openRouter = this.name === "openrouter";
|
|
18022
|
+
const res = await fetch(this.endpoint, {
|
|
17991
18023
|
method: "POST",
|
|
17992
18024
|
headers: {
|
|
17993
18025
|
"content-type": "application/json",
|
|
17994
|
-
authorization: `Bearer ${this.apiKey}
|
|
17995
|
-
"HTTP-Referer": "https://devstation.online",
|
|
17996
|
-
"X-Title": "DevStation"
|
|
18026
|
+
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {},
|
|
18027
|
+
...openRouter ? { "HTTP-Referer": "https://devstation.online", "X-Title": "DevStation" } : {}
|
|
17997
18028
|
},
|
|
17998
18029
|
body: JSON.stringify({
|
|
17999
18030
|
model: this.model,
|
|
@@ -18011,13 +18042,14 @@ class OpenRouterProvider {
|
|
|
18011
18042
|
}
|
|
18012
18043
|
}))
|
|
18013
18044
|
} : {},
|
|
18014
|
-
...process.env.AI_REASONING
|
|
18045
|
+
...openRouter && process.env.AI_REASONING !== "on" ? { reasoning: { enabled: false } } : {}
|
|
18015
18046
|
}),
|
|
18016
18047
|
signal: input.signal
|
|
18017
18048
|
});
|
|
18018
18049
|
if (!res.ok || !res.body) {
|
|
18019
18050
|
const detail = await res.text().catch(() => "");
|
|
18020
|
-
|
|
18051
|
+
const label = openRouter ? "OpenRouter" : `The endpoint ${this.endpoint}`;
|
|
18052
|
+
throw new Error(`${label} request failed (${res.status}). ${detail.slice(0, 200)}`);
|
|
18021
18053
|
}
|
|
18022
18054
|
let text = "";
|
|
18023
18055
|
let finish = null;
|
|
@@ -18085,21 +18117,247 @@ class OpenRouterProvider {
|
|
|
18085
18117
|
};
|
|
18086
18118
|
}
|
|
18087
18119
|
}
|
|
18088
|
-
// src/lib/agent/providers/
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
|
|
18092
|
-
|
|
18093
|
-
|
|
18094
|
-
|
|
18095
|
-
|
|
18120
|
+
// src/lib/agent/providers/settings.ts
|
|
18121
|
+
import {
|
|
18122
|
+
chmodSync,
|
|
18123
|
+
existsSync as existsSync4,
|
|
18124
|
+
mkdirSync as mkdirSync3,
|
|
18125
|
+
readFileSync as readFileSync3,
|
|
18126
|
+
renameSync,
|
|
18127
|
+
statSync as statSync2,
|
|
18128
|
+
writeFileSync as writeFileSync3
|
|
18129
|
+
} from "fs";
|
|
18130
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
18131
|
+
var PROVIDER_IDS = ["anthropic", "openrouter", "openai"];
|
|
18132
|
+
var SETTING_KEYS = ["provider", "model", "baseUrl"];
|
|
18133
|
+
var KEY_ENV = {
|
|
18134
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
18135
|
+
openrouter: ["OPENROUTER_API_KEY", "AI_API_KEY"],
|
|
18136
|
+
openai: ["OPENAI_API_KEY"]
|
|
18137
|
+
};
|
|
18138
|
+
var DEFAULT_BASE_URL = {
|
|
18139
|
+
anthropic: "https://api.anthropic.com",
|
|
18140
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
18141
|
+
openai: "https://api.openai.com/v1"
|
|
18142
|
+
};
|
|
18143
|
+
function globalDir(home) {
|
|
18144
|
+
return join5(home, ".devstation");
|
|
18096
18145
|
}
|
|
18097
|
-
function
|
|
18098
|
-
|
|
18099
|
-
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
18146
|
+
function globalConfigPath(home) {
|
|
18147
|
+
return join5(globalDir(home), "config.json");
|
|
18148
|
+
}
|
|
18149
|
+
function projectConfigPath(root) {
|
|
18150
|
+
return join5(root, ".devstation", "config.json");
|
|
18151
|
+
}
|
|
18152
|
+
function credentialsPath(home) {
|
|
18153
|
+
return join5(globalDir(home), "credentials.json");
|
|
18154
|
+
}
|
|
18155
|
+
function isProvider(value) {
|
|
18156
|
+
return typeof value === "string" && PROVIDER_IDS.includes(value);
|
|
18157
|
+
}
|
|
18158
|
+
function readSettingsFile(path4, problems = []) {
|
|
18159
|
+
if (!existsSync4(path4))
|
|
18160
|
+
return {};
|
|
18161
|
+
try {
|
|
18162
|
+
const raw = JSON.parse(readFileSync3(path4, "utf8"));
|
|
18163
|
+
const out = {};
|
|
18164
|
+
if (raw.provider !== undefined) {
|
|
18165
|
+
const named = typeof raw.provider === "string" ? raw.provider.toLowerCase() : raw.provider;
|
|
18166
|
+
if (isProvider(named))
|
|
18167
|
+
out.provider = named;
|
|
18168
|
+
else
|
|
18169
|
+
problems.push(`${path4}: unknown provider "${String(raw.provider)}".`);
|
|
18170
|
+
}
|
|
18171
|
+
if (typeof raw.model === "string" && raw.model.trim())
|
|
18172
|
+
out.model = raw.model.trim();
|
|
18173
|
+
if (typeof raw.baseUrl === "string" && raw.baseUrl.trim()) {
|
|
18174
|
+
out.baseUrl = raw.baseUrl.trim().replace(/\/+$/, "");
|
|
18175
|
+
}
|
|
18176
|
+
if ("apiKey" in raw) {
|
|
18177
|
+
problems.push(`${path4} contains "apiKey", which is ignored. Keys belong in ${"~/.devstation/credentials.json"}: run \`devstation login\`.`);
|
|
18178
|
+
}
|
|
18179
|
+
return out;
|
|
18180
|
+
} catch {
|
|
18181
|
+
problems.push(`${path4} is not valid JSON, so it was ignored.`);
|
|
18182
|
+
return {};
|
|
18183
|
+
}
|
|
18184
|
+
}
|
|
18185
|
+
function writeJson(path4, value, mode) {
|
|
18186
|
+
mkdirSync3(dirname4(path4), { recursive: true, mode: 448 });
|
|
18187
|
+
const partial = `${path4}.partial`;
|
|
18188
|
+
writeFileSync3(partial, `${JSON.stringify(value, null, 2)}
|
|
18189
|
+
`, { mode });
|
|
18190
|
+
chmodSync(partial, mode);
|
|
18191
|
+
renameSync(partial, path4);
|
|
18192
|
+
}
|
|
18193
|
+
function writeSettingsFile(path4, settings) {
|
|
18194
|
+
const clean = {};
|
|
18195
|
+
for (const key of SETTING_KEYS) {
|
|
18196
|
+
if (settings[key] !== undefined && settings[key] !== "") {
|
|
18197
|
+
clean[key] = settings[key];
|
|
18198
|
+
}
|
|
18199
|
+
}
|
|
18200
|
+
writeJson(path4, clean, 420);
|
|
18201
|
+
}
|
|
18202
|
+
function readCredentials(home, problems = []) {
|
|
18203
|
+
const path4 = credentialsPath(home);
|
|
18204
|
+
if (!existsSync4(path4))
|
|
18205
|
+
return {};
|
|
18206
|
+
try {
|
|
18207
|
+
const mode = statSync2(path4).mode & 511;
|
|
18208
|
+
if (mode & 63) {
|
|
18209
|
+
problems.push(`${path4} is readable by other users (mode ${mode.toString(8)}). Fix it: chmod 600 ${path4}`);
|
|
18210
|
+
}
|
|
18211
|
+
const raw = JSON.parse(readFileSync3(path4, "utf8"));
|
|
18212
|
+
const out = {};
|
|
18213
|
+
for (const id of PROVIDER_IDS) {
|
|
18214
|
+
if (typeof raw[id] === "string" && raw[id].trim())
|
|
18215
|
+
out[id] = raw[id].trim();
|
|
18216
|
+
}
|
|
18217
|
+
return out;
|
|
18218
|
+
} catch {
|
|
18219
|
+
problems.push(`${path4} is not valid JSON, so no stored keys were read.`);
|
|
18220
|
+
return {};
|
|
18221
|
+
}
|
|
18222
|
+
}
|
|
18223
|
+
function writeCredentials(home, credentials) {
|
|
18224
|
+
mkdirSync3(globalDir(home), { recursive: true, mode: 448 });
|
|
18225
|
+
chmodSync(globalDir(home), 448);
|
|
18226
|
+
writeJson(credentialsPath(home), credentials, 384);
|
|
18227
|
+
}
|
|
18228
|
+
function resolveSettings(opts) {
|
|
18229
|
+
const env2 = opts.env ?? process.env;
|
|
18230
|
+
const home = opts.home ?? env2.HOME ?? "";
|
|
18231
|
+
const warnings = [];
|
|
18232
|
+
const project = readSettingsFile(projectConfigPath(opts.root), warnings);
|
|
18233
|
+
const global = home ? readSettingsFile(globalConfigPath(home), warnings) : {};
|
|
18234
|
+
const stored = home ? readCredentials(home, warnings) : {};
|
|
18235
|
+
const projectLabel = ".devstation/config.json";
|
|
18236
|
+
const globalLabel = "~/.devstation/config.json";
|
|
18237
|
+
let provider = null;
|
|
18238
|
+
let providerSource = "not set";
|
|
18239
|
+
if (env2.DEVSTATION_PROVIDER) {
|
|
18240
|
+
const named = env2.DEVSTATION_PROVIDER.toLowerCase();
|
|
18241
|
+
if (isProvider(named)) {
|
|
18242
|
+
provider = named;
|
|
18243
|
+
providerSource = "DEVSTATION_PROVIDER";
|
|
18244
|
+
} else {
|
|
18245
|
+
warnings.push(`DEVSTATION_PROVIDER="${env2.DEVSTATION_PROVIDER}" is not a provider, so it was ignored.`);
|
|
18246
|
+
}
|
|
18247
|
+
}
|
|
18248
|
+
if (!provider && project.provider) {
|
|
18249
|
+
provider = project.provider;
|
|
18250
|
+
providerSource = projectLabel;
|
|
18251
|
+
}
|
|
18252
|
+
if (!provider && global.provider) {
|
|
18253
|
+
provider = global.provider;
|
|
18254
|
+
providerSource = globalLabel;
|
|
18255
|
+
}
|
|
18256
|
+
if (!provider) {
|
|
18257
|
+
for (const id of ["anthropic", "openrouter", "openai"]) {
|
|
18258
|
+
const hit = KEY_ENV[id].find((name) => env2[name]);
|
|
18259
|
+
if (hit) {
|
|
18260
|
+
provider = id;
|
|
18261
|
+
providerSource = `inferred from ${hit}`;
|
|
18262
|
+
break;
|
|
18263
|
+
}
|
|
18264
|
+
}
|
|
18265
|
+
}
|
|
18266
|
+
if (!provider) {
|
|
18267
|
+
const only = PROVIDER_IDS.filter((id) => stored[id]);
|
|
18268
|
+
if (only.length >= 1) {
|
|
18269
|
+
provider = only[0];
|
|
18270
|
+
providerSource = "inferred from ~/.devstation/credentials.json";
|
|
18271
|
+
}
|
|
18272
|
+
}
|
|
18273
|
+
let model = null;
|
|
18274
|
+
let modelSource = "provider default";
|
|
18275
|
+
if (opts.model) {
|
|
18276
|
+
model = opts.model;
|
|
18277
|
+
modelSource = "--model";
|
|
18278
|
+
} else if (env2.DEVSTATION_MODEL) {
|
|
18279
|
+
model = env2.DEVSTATION_MODEL;
|
|
18280
|
+
modelSource = "DEVSTATION_MODEL";
|
|
18281
|
+
} else if (project.model) {
|
|
18282
|
+
model = project.model;
|
|
18283
|
+
modelSource = projectLabel;
|
|
18284
|
+
} else if (global.model) {
|
|
18285
|
+
model = global.model;
|
|
18286
|
+
modelSource = globalLabel;
|
|
18287
|
+
}
|
|
18288
|
+
let baseUrl = null;
|
|
18289
|
+
let baseUrlSource = "provider default";
|
|
18290
|
+
if (env2.DEVSTATION_BASE_URL) {
|
|
18291
|
+
baseUrl = env2.DEVSTATION_BASE_URL.replace(/\/+$/, "");
|
|
18292
|
+
baseUrlSource = "DEVSTATION_BASE_URL";
|
|
18293
|
+
} else if (project.baseUrl) {
|
|
18294
|
+
baseUrl = project.baseUrl;
|
|
18295
|
+
baseUrlSource = projectLabel;
|
|
18296
|
+
} else if (global.baseUrl) {
|
|
18297
|
+
baseUrl = global.baseUrl;
|
|
18298
|
+
baseUrlSource = globalLabel;
|
|
18299
|
+
}
|
|
18300
|
+
let apiKey = null;
|
|
18301
|
+
let keySource = "none";
|
|
18302
|
+
if (provider) {
|
|
18303
|
+
const hit = KEY_ENV[provider].find((name) => env2[name]);
|
|
18304
|
+
if (hit) {
|
|
18305
|
+
apiKey = env2[hit];
|
|
18306
|
+
keySource = hit;
|
|
18307
|
+
} else if (stored[provider]) {
|
|
18308
|
+
apiKey = stored[provider];
|
|
18309
|
+
keySource = "~/.devstation/credentials.json";
|
|
18310
|
+
}
|
|
18311
|
+
}
|
|
18312
|
+
let problem = null;
|
|
18313
|
+
if (!provider) {
|
|
18314
|
+
problem = "No model provider is configured. Run `devstation login`, or set ANTHROPIC_API_KEY or OPENROUTER_API_KEY.";
|
|
18315
|
+
} else if (!apiKey && provider !== "openai") {
|
|
18316
|
+
problem = `No API key for ${provider}. Run \`devstation login ${provider}\`, or set ${KEY_ENV[provider][0]}.`;
|
|
18317
|
+
} else if (provider === "openai" && !apiKey && !baseUrl) {
|
|
18318
|
+
problem = "No API key for openai. Run `devstation login openai`, set OPENAI_API_KEY, or point baseUrl at a local server that needs none.";
|
|
18319
|
+
} else if (provider === "openai" && !model) {
|
|
18320
|
+
problem = "The openai provider needs a model name, because every compatible server names them differently. Run `devstation config set model <name>`.";
|
|
18321
|
+
}
|
|
18322
|
+
return {
|
|
18323
|
+
provider,
|
|
18324
|
+
model,
|
|
18325
|
+
baseUrl,
|
|
18326
|
+
apiKey,
|
|
18327
|
+
source: {
|
|
18328
|
+
provider: providerSource,
|
|
18329
|
+
model: modelSource,
|
|
18330
|
+
baseUrl: baseUrlSource,
|
|
18331
|
+
apiKey: keySource
|
|
18332
|
+
},
|
|
18333
|
+
problem,
|
|
18334
|
+
warnings
|
|
18335
|
+
};
|
|
18336
|
+
}
|
|
18337
|
+
function maskKey(key) {
|
|
18338
|
+
if (!key)
|
|
18339
|
+
return "none";
|
|
18340
|
+
if (key.length <= 8)
|
|
18341
|
+
return "set";
|
|
18342
|
+
return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
|
|
18343
|
+
}
|
|
18344
|
+
// src/lib/agent/providers/index.ts
|
|
18345
|
+
function providerFromSettings(resolved) {
|
|
18346
|
+
if (resolved.problem || !resolved.provider)
|
|
18347
|
+
return null;
|
|
18348
|
+
const model = resolved.model ?? undefined;
|
|
18349
|
+
const baseUrl = resolved.baseUrl ?? undefined;
|
|
18350
|
+
switch (resolved.provider) {
|
|
18351
|
+
case "anthropic":
|
|
18352
|
+
return new AnthropicProvider({ apiKey: resolved.apiKey ?? undefined, model, baseUrl });
|
|
18353
|
+
case "openrouter":
|
|
18354
|
+
return new OpenRouterProvider(resolved.apiKey ?? "", model, { name: "openrouter", baseUrl });
|
|
18355
|
+
case "openai":
|
|
18356
|
+
return new OpenRouterProvider(resolved.apiKey ?? "", model, {
|
|
18357
|
+
name: "openai",
|
|
18358
|
+
baseUrl: baseUrl ?? "https://api.openai.com/v1"
|
|
18359
|
+
});
|
|
18360
|
+
}
|
|
18103
18361
|
}
|
|
18104
18362
|
|
|
18105
18363
|
// src/lib/agent/memory/embeddings.ts
|
|
@@ -18195,11 +18453,11 @@ async function embedMissing(store, provider, options = {}) {
|
|
|
18195
18453
|
}
|
|
18196
18454
|
|
|
18197
18455
|
// src/lib/agent/memory/workspace-index.ts
|
|
18198
|
-
import { join as
|
|
18456
|
+
import { join as join7 } from "path";
|
|
18199
18457
|
|
|
18200
18458
|
// src/lib/agent/repo-session.ts
|
|
18201
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
18202
|
-
import { join as
|
|
18459
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
18460
|
+
import { join as join6, relative as relative2, sep as sep2 } from "path";
|
|
18203
18461
|
var SKIP_DIRS = new Set([
|
|
18204
18462
|
".git",
|
|
18205
18463
|
".agent",
|
|
@@ -18233,7 +18491,7 @@ function readWorkspace(root, maxFileBytes = 1024 * 1024) {
|
|
|
18233
18491
|
for (const item of readdirSync3(dir, { withFileTypes: true })) {
|
|
18234
18492
|
if (item.isSymbolicLink())
|
|
18235
18493
|
continue;
|
|
18236
|
-
const full =
|
|
18494
|
+
const full = join6(dir, item.name);
|
|
18237
18495
|
if (item.isDirectory()) {
|
|
18238
18496
|
if (SKIP_DIRS.has(item.name))
|
|
18239
18497
|
continue;
|
|
@@ -18242,9 +18500,9 @@ function readWorkspace(root, maxFileBytes = 1024 * 1024) {
|
|
|
18242
18500
|
}
|
|
18243
18501
|
if (!item.isFile())
|
|
18244
18502
|
continue;
|
|
18245
|
-
if (
|
|
18503
|
+
if (statSync3(full).size > maxFileBytes)
|
|
18246
18504
|
continue;
|
|
18247
|
-
const buffer =
|
|
18505
|
+
const buffer = readFileSync4(full);
|
|
18248
18506
|
if (looksBinary(buffer))
|
|
18249
18507
|
continue;
|
|
18250
18508
|
files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
|
|
@@ -18281,8 +18539,8 @@ ${goal}` };
|
|
|
18281
18539
|
// src/lib/agent/memory/store.ts
|
|
18282
18540
|
import { Database } from "bun:sqlite";
|
|
18283
18541
|
import { createHash } from "crypto";
|
|
18284
|
-
import { mkdirSync as
|
|
18285
|
-
import { dirname as
|
|
18542
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
18543
|
+
import { dirname as dirname5 } from "path";
|
|
18286
18544
|
|
|
18287
18545
|
// src/lib/agent/memory/chunk.ts
|
|
18288
18546
|
var BRACE_LANGUAGES = new Set([
|
|
@@ -18542,7 +18800,7 @@ class MemoryStore {
|
|
|
18542
18800
|
constructor(path4) {
|
|
18543
18801
|
this.path = path4;
|
|
18544
18802
|
if (path4 !== ":memory:")
|
|
18545
|
-
|
|
18803
|
+
mkdirSync4(dirname5(path4), { recursive: true });
|
|
18546
18804
|
this.db = new Database(path4);
|
|
18547
18805
|
this.db.run("PRAGMA journal_mode = WAL");
|
|
18548
18806
|
this.migrate();
|
|
@@ -18715,7 +18973,7 @@ function toChunk(row) {
|
|
|
18715
18973
|
|
|
18716
18974
|
// src/lib/agent/memory/workspace-index.ts
|
|
18717
18975
|
function storePath(root) {
|
|
18718
|
-
return
|
|
18976
|
+
return join7(root, ".agent", "memory.db");
|
|
18719
18977
|
}
|
|
18720
18978
|
function openStore(root) {
|
|
18721
18979
|
return new MemoryStore(storePath(root));
|
|
@@ -18735,8 +18993,8 @@ async function indexWorkspace(root, options = {}) {
|
|
|
18735
18993
|
}
|
|
18736
18994
|
|
|
18737
18995
|
// src/lib/agent/memory/project-memory.ts
|
|
18738
|
-
import { existsSync as
|
|
18739
|
-
import { dirname as
|
|
18996
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
18997
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
18740
18998
|
var DEFAULT_FILE = "PROJECT_MEMORY.md";
|
|
18741
18999
|
var HEADER = `# Project memory
|
|
18742
19000
|
|
|
@@ -18745,16 +19003,16 @@ things about this project that are not in the code. Safe to edit or delete by
|
|
|
18745
19003
|
hand: the agent only ever appends.
|
|
18746
19004
|
`;
|
|
18747
19005
|
function memoryPath(root) {
|
|
18748
|
-
const atRoot =
|
|
18749
|
-
if (
|
|
19006
|
+
const atRoot = join8(root, DEFAULT_FILE);
|
|
19007
|
+
if (existsSync5(atRoot))
|
|
18750
19008
|
return atRoot;
|
|
18751
|
-
return
|
|
19009
|
+
return join8(root, ".agent", DEFAULT_FILE);
|
|
18752
19010
|
}
|
|
18753
19011
|
function readMemory(root) {
|
|
18754
19012
|
const path4 = memoryPath(root);
|
|
18755
|
-
if (!
|
|
19013
|
+
if (!existsSync5(path4))
|
|
18756
19014
|
return [];
|
|
18757
|
-
return parseMemory(
|
|
19015
|
+
return parseMemory(readFileSync5(path4, "utf8"));
|
|
18758
19016
|
}
|
|
18759
19017
|
var ENTRY = /^- \[([^\]]+)\](?:\s*\(([^)]*)\))?\s+([\s\S]*)$/;
|
|
18760
19018
|
function parseMemory(text) {
|
|
@@ -18778,16 +19036,16 @@ function remember(root, note, tag = null, now = new Date) {
|
|
|
18778
19036
|
if (!text) {
|
|
18779
19037
|
return { ok: false, path: path4, message: "There was nothing to remember." };
|
|
18780
19038
|
}
|
|
18781
|
-
const existing =
|
|
19039
|
+
const existing = existsSync5(path4) ? readFileSync5(path4, "utf8") : "";
|
|
18782
19040
|
const entries = parseMemory(existing);
|
|
18783
19041
|
const normal = (value) => value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
18784
19042
|
if (entries.some((entry) => normal(entry.note) === normal(text))) {
|
|
18785
19043
|
return { ok: true, path: path4, duplicate: true, message: "Already remembered; nothing was added." };
|
|
18786
19044
|
}
|
|
18787
19045
|
const line = formatEntry({ note: text, tag, at: now.toISOString() });
|
|
18788
|
-
|
|
19046
|
+
mkdirSync5(dirname6(path4), { recursive: true });
|
|
18789
19047
|
const body = existing || HEADER;
|
|
18790
|
-
|
|
19048
|
+
writeFileSync4(path4, `${body.replace(/\n+$/, "")}
|
|
18791
19049
|
${line}
|
|
18792
19050
|
`);
|
|
18793
19051
|
return { ok: true, path: path4, message: `Remembered, in ${DEFAULT_FILE}.` };
|
|
@@ -18814,22 +19072,22 @@ function renderMemory(entries) {
|
|
|
18814
19072
|
|
|
18815
19073
|
// src/lib/agent/mcp.ts
|
|
18816
19074
|
import { spawn as spawn3 } from "child_process";
|
|
18817
|
-
import { existsSync as
|
|
18818
|
-
import { join as
|
|
19075
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
19076
|
+
import { join as join9 } from "path";
|
|
18819
19077
|
function configPaths(root, home = process.env.HOME ?? "") {
|
|
18820
19078
|
return [
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
...home ? [
|
|
19079
|
+
join9(root, ".devstation", "mcp.json"),
|
|
19080
|
+
join9(root, ".mcp.json"),
|
|
19081
|
+
...home ? [join9(home, ".devstation", "mcp.json")] : []
|
|
18824
19082
|
];
|
|
18825
19083
|
}
|
|
18826
19084
|
function loadConfig(root, home = process.env.HOME ?? "") {
|
|
18827
19085
|
const merged = { mcpServers: {} };
|
|
18828
19086
|
for (const path4 of configPaths(root, home).reverse()) {
|
|
18829
|
-
if (!
|
|
19087
|
+
if (!existsSync6(path4))
|
|
18830
19088
|
continue;
|
|
18831
19089
|
try {
|
|
18832
|
-
const parsed = JSON.parse(
|
|
19090
|
+
const parsed = JSON.parse(readFileSync6(path4, "utf8"));
|
|
18833
19091
|
for (const [name, server] of Object.entries(parsed.mcpServers ?? {})) {
|
|
18834
19092
|
if (server && typeof server.command === "string")
|
|
18835
19093
|
merged.mcpServers[name] = server;
|
|
@@ -19089,12 +19347,12 @@ function failedResult(message) {
|
|
|
19089
19347
|
|
|
19090
19348
|
// src/lib/agent/sandbox-exec.ts
|
|
19091
19349
|
import { randomBytes } from "crypto";
|
|
19092
|
-
import { existsSync as
|
|
19093
|
-
import { isAbsolute as isAbsolute3, join as
|
|
19350
|
+
import { existsSync as existsSync8, statSync as statSync5, unlinkSync } from "fs";
|
|
19351
|
+
import { isAbsolute as isAbsolute3, join as join11, relative as relative4, resolve as resolve3, sep as sep4 } from "path";
|
|
19094
19352
|
|
|
19095
19353
|
// src/lib/agent/project.ts
|
|
19096
|
-
import { existsSync as
|
|
19097
|
-
import { dirname as
|
|
19354
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
19355
|
+
import { dirname as dirname7, join as join10, relative as relative3, sep as sep3 } from "path";
|
|
19098
19356
|
var SKIP = new Set([
|
|
19099
19357
|
"node_modules",
|
|
19100
19358
|
".git",
|
|
@@ -19120,7 +19378,7 @@ var MANIFESTS = [
|
|
|
19120
19378
|
var MAX_DEPTH = 2;
|
|
19121
19379
|
function readScripts(absolute) {
|
|
19122
19380
|
try {
|
|
19123
|
-
const parsed = JSON.parse(
|
|
19381
|
+
const parsed = JSON.parse(readFileSync7(absolute, "utf8"));
|
|
19124
19382
|
return parsed.scripts ?? {};
|
|
19125
19383
|
} catch {
|
|
19126
19384
|
return {};
|
|
@@ -19130,8 +19388,8 @@ function detectManifests(root) {
|
|
|
19130
19388
|
const found = [];
|
|
19131
19389
|
const scan = (dir, depth) => {
|
|
19132
19390
|
for (const { file, ecosystem } of MANIFESTS) {
|
|
19133
|
-
const absolute =
|
|
19134
|
-
if (!
|
|
19391
|
+
const absolute = join10(dir, file);
|
|
19392
|
+
if (!existsSync7(absolute))
|
|
19135
19393
|
continue;
|
|
19136
19394
|
found.push({
|
|
19137
19395
|
dir: relative3(root, dir).split(sep3).join("/"),
|
|
@@ -19151,9 +19409,9 @@ function detectManifests(root) {
|
|
|
19151
19409
|
for (const entry of entries) {
|
|
19152
19410
|
if (SKIP.has(entry) || entry.startsWith("."))
|
|
19153
19411
|
continue;
|
|
19154
|
-
const child =
|
|
19412
|
+
const child = join10(dir, entry);
|
|
19155
19413
|
try {
|
|
19156
|
-
if (
|
|
19414
|
+
if (statSync4(child).isDirectory())
|
|
19157
19415
|
scan(child, depth + 1);
|
|
19158
19416
|
} catch {}
|
|
19159
19417
|
}
|
|
@@ -19240,7 +19498,7 @@ function lockfilesFor(manifest) {
|
|
|
19240
19498
|
}
|
|
19241
19499
|
}
|
|
19242
19500
|
function cwdFor(root, manifest) {
|
|
19243
|
-
return manifest.dir ?
|
|
19501
|
+
return manifest.dir ? join10(root, manifest.dir) : root;
|
|
19244
19502
|
}
|
|
19245
19503
|
|
|
19246
19504
|
// src/lib/agent/sandbox-exec.ts
|
|
@@ -19294,7 +19552,7 @@ var NEEDS = {
|
|
|
19294
19552
|
go: ["go"]
|
|
19295
19553
|
};
|
|
19296
19554
|
async function probeImage(workspace, image, runtime) {
|
|
19297
|
-
const ecosystems =
|
|
19555
|
+
const ecosystems = gatingEcosystems(detectManifests(workspace));
|
|
19298
19556
|
const wanted = [...new Set(ecosystems.flatMap((e) => NEEDS[e]))];
|
|
19299
19557
|
if (wanted.length === 0)
|
|
19300
19558
|
return { ok: true, missing: [], ecosystems };
|
|
@@ -19306,10 +19564,13 @@ async function probeImage(workspace, image, runtime) {
|
|
|
19306
19564
|
`).map((l) => l.trim()).filter(Boolean);
|
|
19307
19565
|
return { ok: missing.length === 0, missing, ecosystems };
|
|
19308
19566
|
}
|
|
19567
|
+
function gatingEcosystems(manifests) {
|
|
19568
|
+
return [...new Set(manifests.filter((m) => m.dir === "").map((m) => m.ecosystem))];
|
|
19569
|
+
}
|
|
19309
19570
|
function probeProblem(probe, image) {
|
|
19310
19571
|
if (probe.ok)
|
|
19311
19572
|
return null;
|
|
19312
|
-
return `
|
|
19573
|
+
return `The sandbox image ${image} has no ${probe.missing.join(", ")}, which this ${probe.ecosystems.join(" and ")} ` + "project uses, so commands that need it will fail inside the sandbox. Build an image that has it " + "and set DEVSTATION_SANDBOX_IMAGE, or run with --no-sandbox.";
|
|
19313
19574
|
}
|
|
19314
19575
|
function quote2(value) {
|
|
19315
19576
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -19415,16 +19676,16 @@ function sandboxExecutor(options) {
|
|
|
19415
19676
|
};
|
|
19416
19677
|
const verifyOwnership = async (name) => {
|
|
19417
19678
|
const probe = `.devstation-uid-probe-${session}`;
|
|
19418
|
-
const path4 =
|
|
19679
|
+
const path4 = join11(options.workspace, probe);
|
|
19419
19680
|
const written = await runShell(execCommand(name, `touch ${quote2(probe)}`, 20), {
|
|
19420
19681
|
cwd: options.workspace,
|
|
19421
19682
|
timeoutMs: 30000
|
|
19422
19683
|
});
|
|
19423
|
-
if (!written.ok || !
|
|
19684
|
+
if (!written.ok || !existsSync8(path4)) {
|
|
19424
19685
|
return "The sandbox could not write to the workspace. Check the mount and try --no-sandbox.";
|
|
19425
19686
|
}
|
|
19426
19687
|
try {
|
|
19427
|
-
const stat2 =
|
|
19688
|
+
const stat2 = statSync5(path4);
|
|
19428
19689
|
const [uid, gid] = user.split(":").map(Number);
|
|
19429
19690
|
if (stat2.uid !== uid || stat2.gid !== gid) {
|
|
19430
19691
|
return `The sandbox writes files as ${stat2.uid}:${stat2.gid} but this account is ${user}. ` + "Files it creates would not be yours to edit, and git would stop trusting the " + "repository, so it was not started. Run with --no-sandbox, or set " + "DEVSTATION_SANDBOX_USER.";
|
|
@@ -20475,37 +20736,37 @@ function summarise(call, outcome) {
|
|
|
20475
20736
|
// src/lib/agent/session-store.ts
|
|
20476
20737
|
import {
|
|
20477
20738
|
appendFileSync,
|
|
20478
|
-
existsSync as
|
|
20479
|
-
mkdirSync as
|
|
20480
|
-
readFileSync as
|
|
20739
|
+
existsSync as existsSync9,
|
|
20740
|
+
mkdirSync as mkdirSync6,
|
|
20741
|
+
readFileSync as readFileSync8,
|
|
20481
20742
|
readdirSync as readdirSync6,
|
|
20482
|
-
renameSync,
|
|
20483
|
-
statSync as
|
|
20484
|
-
writeFileSync as
|
|
20743
|
+
renameSync as renameSync2,
|
|
20744
|
+
statSync as statSync6,
|
|
20745
|
+
writeFileSync as writeFileSync5
|
|
20485
20746
|
} from "fs";
|
|
20486
|
-
import { join as
|
|
20747
|
+
import { join as join12 } from "path";
|
|
20487
20748
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
20488
|
-
var SESSIONS_DIR =
|
|
20749
|
+
var SESSIONS_DIR = join12(".agent", "sessions");
|
|
20489
20750
|
|
|
20490
20751
|
class SessionStore {
|
|
20491
20752
|
root;
|
|
20492
20753
|
dir;
|
|
20493
20754
|
constructor(root) {
|
|
20494
20755
|
this.root = root;
|
|
20495
|
-
this.dir =
|
|
20756
|
+
this.dir = join12(root, SESSIONS_DIR);
|
|
20496
20757
|
}
|
|
20497
20758
|
ensure() {
|
|
20498
|
-
|
|
20499
|
-
const ignore =
|
|
20500
|
-
if (!
|
|
20501
|
-
|
|
20759
|
+
mkdirSync6(this.dir, { recursive: true });
|
|
20760
|
+
const ignore = join12(this.dir, "..", ".gitignore");
|
|
20761
|
+
if (!existsSync9(ignore))
|
|
20762
|
+
writeFileSync5(ignore, `*
|
|
20502
20763
|
`);
|
|
20503
20764
|
}
|
|
20504
20765
|
jsonPath(id) {
|
|
20505
|
-
return
|
|
20766
|
+
return join12(this.dir, `${id}.json`);
|
|
20506
20767
|
}
|
|
20507
20768
|
eventPath(id) {
|
|
20508
|
-
return
|
|
20769
|
+
return join12(this.dir, `${id}.jsonl`);
|
|
20509
20770
|
}
|
|
20510
20771
|
create(goal, meta) {
|
|
20511
20772
|
this.ensure();
|
|
@@ -20535,21 +20796,21 @@ class SessionStore {
|
|
|
20535
20796
|
record.updatedAt = new Date().toISOString();
|
|
20536
20797
|
const target = this.jsonPath(record.id);
|
|
20537
20798
|
const temporary = `${target}.tmp`;
|
|
20538
|
-
|
|
20539
|
-
|
|
20799
|
+
writeFileSync5(temporary, JSON.stringify(record, null, 2));
|
|
20800
|
+
renameSync2(temporary, target);
|
|
20540
20801
|
}
|
|
20541
20802
|
load(id) {
|
|
20542
20803
|
const path4 = this.jsonPath(id);
|
|
20543
|
-
if (!
|
|
20804
|
+
if (!existsSync9(path4))
|
|
20544
20805
|
return null;
|
|
20545
20806
|
try {
|
|
20546
|
-
return JSON.parse(
|
|
20807
|
+
return JSON.parse(readFileSync8(path4, "utf8"));
|
|
20547
20808
|
} catch {
|
|
20548
20809
|
return null;
|
|
20549
20810
|
}
|
|
20550
20811
|
}
|
|
20551
20812
|
list() {
|
|
20552
|
-
if (!
|
|
20813
|
+
if (!existsSync9(this.dir))
|
|
20553
20814
|
return [];
|
|
20554
20815
|
return readdirSync6(this.dir).filter((name) => name.endsWith(".json")).map((name) => this.load(name.slice(0, -".json".length))).filter((record) => record !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
20555
20816
|
}
|
|
@@ -20563,12 +20824,12 @@ class SessionStore {
|
|
|
20563
20824
|
}
|
|
20564
20825
|
readEvents(id, fromByte = 0) {
|
|
20565
20826
|
const path4 = this.eventPath(id);
|
|
20566
|
-
if (!
|
|
20827
|
+
if (!existsSync9(path4))
|
|
20567
20828
|
return { events: [], offset: 0 };
|
|
20568
|
-
const size =
|
|
20829
|
+
const size = statSync6(path4).size;
|
|
20569
20830
|
if (size <= fromByte)
|
|
20570
20831
|
return { events: [], offset: size };
|
|
20571
|
-
const text =
|
|
20832
|
+
const text = readFileSync8(path4, "utf8").slice(fromByte);
|
|
20572
20833
|
const events = [];
|
|
20573
20834
|
let consumed = 0;
|
|
20574
20835
|
for (const line of text.split(`
|
|
@@ -20663,7 +20924,8 @@ function renderSessions(records) {
|
|
|
20663
20924
|
}
|
|
20664
20925
|
|
|
20665
20926
|
// src/lib/agent/cli/commands.ts
|
|
20666
|
-
async function buildExecutor(root, sandbox) {
|
|
20927
|
+
async function buildExecutor(root, sandbox, warn = (message) => process.stderr.write(`warning: ${message}
|
|
20928
|
+
`)) {
|
|
20667
20929
|
if (!sandbox)
|
|
20668
20930
|
return { executor: hostExecutor() };
|
|
20669
20931
|
const readiness = await sandboxReadiness();
|
|
@@ -20673,7 +20935,7 @@ async function buildExecutor(root, sandbox) {
|
|
|
20673
20935
|
const probe = await probeImage(root, readiness.imageName, readiness.runtimeName);
|
|
20674
20936
|
const mismatch = probeProblem(probe, readiness.imageName);
|
|
20675
20937
|
if (mismatch)
|
|
20676
|
-
|
|
20938
|
+
warn(mismatch);
|
|
20677
20939
|
return { executor: sandboxExecutor({ workspace: root }) };
|
|
20678
20940
|
}
|
|
20679
20941
|
function isYes(answer) {
|
|
@@ -20962,7 +21224,17 @@ function configCommand(context) {
|
|
|
20962
21224
|
context.terminal.out(JSON.stringify({
|
|
20963
21225
|
workspace: context.root,
|
|
20964
21226
|
git: isRepo(context.root),
|
|
20965
|
-
provider: context.provider ? { name: context.provider.name, model: context.provider.model } :
|
|
21227
|
+
provider: context.provider ? { name: context.provider.name, model: context.provider.model } : (() => {
|
|
21228
|
+
const r2 = resolveSettings({ root: context.root });
|
|
21229
|
+
return {
|
|
21230
|
+
name: r2.provider,
|
|
21231
|
+
model: r2.model,
|
|
21232
|
+
baseUrl: r2.baseUrl,
|
|
21233
|
+
key: maskKey(r2.apiKey),
|
|
21234
|
+
source: r2.source,
|
|
21235
|
+
problem: r2.problem
|
|
21236
|
+
};
|
|
21237
|
+
})(),
|
|
20966
21238
|
autonomy: context.autonomy ?? "ask_sensitive",
|
|
20967
21239
|
maxSteps: context.maxSteps ?? 40,
|
|
20968
21240
|
budgetUsd: context.maxCostUsd ?? null,
|
|
@@ -20971,10 +21243,14 @@ function configCommand(context) {
|
|
|
20971
21243
|
}, null, 2));
|
|
20972
21244
|
return 0;
|
|
20973
21245
|
}
|
|
21246
|
+
const r = resolveSettings({ root: context.root });
|
|
20974
21247
|
const lines = [
|
|
20975
21248
|
`workspace ${context.root}`,
|
|
20976
21249
|
`git ${isRepo(context.root) ? "yes" : "no, so checkpoints are file snapshots under .devstation/"}`,
|
|
20977
|
-
|
|
21250
|
+
context.provider ? `provider ${context.provider.name}/${context.provider.model} (this session)` : `provider ${r.provider ?? "none"} (${r.source.provider})`,
|
|
21251
|
+
`model ${context.provider ? context.provider.model : r.model ?? "provider default"} (${context.provider ? "this session" : r.source.model})`,
|
|
21252
|
+
`endpoint ${r.baseUrl ?? (r.provider ? DEFAULT_BASE_URL[r.provider] : "none")} (${r.source.baseUrl})`,
|
|
21253
|
+
`api key ${maskKey(r.apiKey)} (${r.source.apiKey})`,
|
|
20978
21254
|
`autonomy ${context.autonomy ?? "ask_sensitive"}`,
|
|
20979
21255
|
`max steps ${context.maxSteps ?? 40}`,
|
|
20980
21256
|
`budget ${context.maxCostUsd ? `$${context.maxCostUsd}` : "none set"}`,
|
|
@@ -20982,16 +21258,171 @@ function configCommand(context) {
|
|
|
20982
21258
|
];
|
|
20983
21259
|
for (const line of lines)
|
|
20984
21260
|
context.terminal.out(line);
|
|
21261
|
+
if (r.problem && !context.provider)
|
|
21262
|
+
context.terminal.err(`
|
|
21263
|
+
${r.problem}`);
|
|
21264
|
+
for (const warning of r.warnings)
|
|
21265
|
+
context.terminal.err(`warning: ${warning}`);
|
|
21266
|
+
return 0;
|
|
21267
|
+
}
|
|
21268
|
+
function home() {
|
|
21269
|
+
return process.env.HOME ?? "";
|
|
21270
|
+
}
|
|
21271
|
+
function homeDirectoryWarning(root, homeDir = process.env.HOME ?? "") {
|
|
21272
|
+
if (!homeDir)
|
|
21273
|
+
return null;
|
|
21274
|
+
const strip2 = (p) => p.replace(/\/+$/, "");
|
|
21275
|
+
if (strip2(root) !== strip2(homeDir))
|
|
21276
|
+
return null;
|
|
21277
|
+
return "You are in your home directory, so the agent treats everything under it as one project, " + "every repository inside it included. cd into the project you mean first.";
|
|
21278
|
+
}
|
|
21279
|
+
function configEditCommand(context, rest, opts = {}) {
|
|
21280
|
+
const [action, key, ...valueWords] = rest.trim().split(/\s+/);
|
|
21281
|
+
const value = valueWords.join(" ").trim();
|
|
21282
|
+
const path4 = opts.project ? projectConfigPath(context.root) : globalConfigPath(home());
|
|
21283
|
+
if (action === "path") {
|
|
21284
|
+
context.terminal.out(`global ${globalConfigPath(home())}`);
|
|
21285
|
+
context.terminal.out(`project ${projectConfigPath(context.root)}`);
|
|
21286
|
+
context.terminal.out(`credentials ${credentialsPath(home())}`);
|
|
21287
|
+
return 0;
|
|
21288
|
+
}
|
|
21289
|
+
if (key === "apiKey" || key === "key") {
|
|
21290
|
+
context.terminal.err(`API keys are not stored in config. Run \`devstation login\` to store one in ${credentialsPath(home())}.`);
|
|
21291
|
+
return 2;
|
|
21292
|
+
}
|
|
21293
|
+
if (!key || !SETTING_KEYS.includes(key)) {
|
|
21294
|
+
context.terminal.err(`Unknown setting "${key ?? ""}". Settings: ${SETTING_KEYS.join(", ")}.`);
|
|
21295
|
+
return 2;
|
|
21296
|
+
}
|
|
21297
|
+
const name = key;
|
|
21298
|
+
const current = readSettingsFile(path4);
|
|
21299
|
+
if (action === "get") {
|
|
21300
|
+
context.terminal.out(current[name] ?? "");
|
|
21301
|
+
return 0;
|
|
21302
|
+
}
|
|
21303
|
+
if (action === "unset") {
|
|
21304
|
+
delete current[name];
|
|
21305
|
+
writeSettingsFile(path4, current);
|
|
21306
|
+
context.terminal.out(`Removed ${name} from ${path4}.`);
|
|
21307
|
+
return 0;
|
|
21308
|
+
}
|
|
21309
|
+
if (action === "set") {
|
|
21310
|
+
if (!value) {
|
|
21311
|
+
context.terminal.err(`Give it a value: devstation config set ${name} <value>`);
|
|
21312
|
+
return 2;
|
|
21313
|
+
}
|
|
21314
|
+
if (name === "provider" && !PROVIDER_IDS.includes(value.toLowerCase())) {
|
|
21315
|
+
context.terminal.err(`Unknown provider "${value}". Providers: ${PROVIDER_IDS.join(", ")}.`);
|
|
21316
|
+
return 2;
|
|
21317
|
+
}
|
|
21318
|
+
if (name === "baseUrl" && !/^https?:\/\//.test(value)) {
|
|
21319
|
+
context.terminal.err("baseUrl must start with http:// or https://.");
|
|
21320
|
+
return 2;
|
|
21321
|
+
}
|
|
21322
|
+
current[name] = name === "baseUrl" ? value.replace(/\/+$/, "") : name === "provider" ? value.toLowerCase() : value;
|
|
21323
|
+
writeSettingsFile(path4, current);
|
|
21324
|
+
context.terminal.out(`Set ${name} = ${current[name]} in ${path4}.`);
|
|
21325
|
+
return 0;
|
|
21326
|
+
}
|
|
21327
|
+
context.terminal.err("Use: devstation config set <key> <value> | get <key> | unset <key> | path (add --project for this workspace)");
|
|
21328
|
+
return 2;
|
|
21329
|
+
}
|
|
21330
|
+
async function loginCommand(context, rest) {
|
|
21331
|
+
const t = context.terminal;
|
|
21332
|
+
const secret = t.askSecret ? (q) => t.askSecret(q) : (q) => t.ask(q);
|
|
21333
|
+
let provider = (rest.trim().split(/\s+/)[0] ?? "").toLowerCase();
|
|
21334
|
+
if (!provider) {
|
|
21335
|
+
t.out("Which provider?");
|
|
21336
|
+
t.out(" anthropic Claude, directly (prompt caching, native tool use)");
|
|
21337
|
+
t.out(" openrouter one key for Claude, GPT, Gemini, DeepSeek and more");
|
|
21338
|
+
t.out(" openai OpenAI, or any compatible server: Ollama, LM Studio, Groq, Together");
|
|
21339
|
+
provider = (await t.ask("provider [anthropic]: ")).trim().toLowerCase() || "anthropic";
|
|
21340
|
+
}
|
|
21341
|
+
if (!PROVIDER_IDS.includes(provider)) {
|
|
21342
|
+
t.err(`Unknown provider "${provider}". Providers: ${PROVIDER_IDS.join(", ")}.`);
|
|
21343
|
+
return 2;
|
|
21344
|
+
}
|
|
21345
|
+
const id = provider;
|
|
21346
|
+
let baseUrl = "";
|
|
21347
|
+
if (id === "openai") {
|
|
21348
|
+
baseUrl = (await t.ask(`endpoint [${DEFAULT_BASE_URL.openai}]: `)).trim();
|
|
21349
|
+
if (baseUrl && !/^https?:\/\//.test(baseUrl)) {
|
|
21350
|
+
t.err("The endpoint must start with http:// or https://.");
|
|
21351
|
+
return 2;
|
|
21352
|
+
}
|
|
21353
|
+
}
|
|
21354
|
+
const key = (await secret(id === "openai" ? "API key (blank for a local server): " : "API key: ")).trim();
|
|
21355
|
+
if (!key && id !== "openai") {
|
|
21356
|
+
t.err("No key entered, so nothing was saved.");
|
|
21357
|
+
return 2;
|
|
21358
|
+
}
|
|
21359
|
+
const modelHint = id === "anthropic" ? "claude-sonnet-5" : id === "openrouter" ? "anthropic/claude-sonnet-5" : "required";
|
|
21360
|
+
const model = (await t.ask(`model [${modelHint}]: `)).trim();
|
|
21361
|
+
if (id === "openai" && !model) {
|
|
21362
|
+
t.err("The openai provider needs a model name, because every compatible server names them differently.");
|
|
21363
|
+
return 2;
|
|
21364
|
+
}
|
|
21365
|
+
const h = home();
|
|
21366
|
+
if (!h) {
|
|
21367
|
+
t.err("HOME is not set, so there is nowhere to store settings.");
|
|
21368
|
+
return 2;
|
|
21369
|
+
}
|
|
21370
|
+
if (key) {
|
|
21371
|
+
const credentials = readCredentials(h);
|
|
21372
|
+
credentials[id] = key;
|
|
21373
|
+
writeCredentials(h, credentials);
|
|
21374
|
+
}
|
|
21375
|
+
const settings2 = readSettingsFile(globalConfigPath(h));
|
|
21376
|
+
settings2.provider = id;
|
|
21377
|
+
if (model)
|
|
21378
|
+
settings2.model = model;
|
|
21379
|
+
else
|
|
21380
|
+
delete settings2.model;
|
|
21381
|
+
if (baseUrl)
|
|
21382
|
+
settings2.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
21383
|
+
else if (id !== "openai")
|
|
21384
|
+
delete settings2.baseUrl;
|
|
21385
|
+
writeSettingsFile(globalConfigPath(h), settings2);
|
|
21386
|
+
t.out("");
|
|
21387
|
+
t.out(`Saved. provider ${id}, model ${model || "provider default"}${baseUrl ? `, endpoint ${baseUrl}` : ""}`);
|
|
21388
|
+
if (key)
|
|
21389
|
+
t.out(`key ${maskKey(key)} in ${credentialsPath(h)} (readable only by you)`);
|
|
21390
|
+
t.out(`settings in ${globalConfigPath(h)}`);
|
|
21391
|
+
t.out("");
|
|
21392
|
+
t.out("Check it with: devstation doctor");
|
|
21393
|
+
return 0;
|
|
21394
|
+
}
|
|
21395
|
+
function logoutCommand(context, rest) {
|
|
21396
|
+
const h = home();
|
|
21397
|
+
const target = (rest.trim().split(/\s+/)[0] ?? "").toLowerCase();
|
|
21398
|
+
if (target && !PROVIDER_IDS.includes(target)) {
|
|
21399
|
+
context.terminal.err(`Unknown provider "${target}". Providers: ${PROVIDER_IDS.join(", ")}.`);
|
|
21400
|
+
return 2;
|
|
21401
|
+
}
|
|
21402
|
+
const credentials = readCredentials(h);
|
|
21403
|
+
const removed = target ? credentials[target] ? [target] : [] : Object.keys(credentials);
|
|
21404
|
+
if (removed.length === 0) {
|
|
21405
|
+
context.terminal.out(target ? `No stored key for ${target}.` : "No stored keys.");
|
|
21406
|
+
return 0;
|
|
21407
|
+
}
|
|
21408
|
+
if (target)
|
|
21409
|
+
delete credentials[target];
|
|
21410
|
+
writeCredentials(h, target ? credentials : {});
|
|
21411
|
+
context.terminal.out(`Removed the stored key for ${removed.join(", ")}.`);
|
|
21412
|
+
context.terminal.out("Keys exported in your shell (ANTHROPIC_API_KEY, OPENROUTER_API_KEY, OPENAI_API_KEY) are not touched.");
|
|
20985
21413
|
return 0;
|
|
20986
21414
|
}
|
|
20987
21415
|
async function runChecks(root, env2 = process.env) {
|
|
20988
21416
|
const checks = [];
|
|
20989
|
-
const
|
|
21417
|
+
const settings2 = resolveSettings({ root, env: env2 });
|
|
20990
21418
|
checks.push({
|
|
20991
21419
|
name: "model provider",
|
|
20992
|
-
ok:
|
|
20993
|
-
detail:
|
|
21420
|
+
ok: settings2.problem === null,
|
|
21421
|
+
detail: settings2.problem ?? `${settings2.provider}${settings2.model ? `/${settings2.model}` : ""}, key from ${settings2.source.apiKey}`
|
|
20994
21422
|
});
|
|
21423
|
+
for (const warning of settings2.warnings) {
|
|
21424
|
+
checks.push({ name: "settings", ok: false, detail: warning });
|
|
21425
|
+
}
|
|
20995
21426
|
const git2 = await runShell("git --version", { cwd: root, timeoutMs: 15000 });
|
|
20996
21427
|
checks.push({
|
|
20997
21428
|
name: "git",
|
|
@@ -21000,7 +21431,7 @@ async function runChecks(root, env2 = process.env) {
|
|
|
21000
21431
|
});
|
|
21001
21432
|
checks.push({
|
|
21002
21433
|
name: "workspace",
|
|
21003
|
-
ok:
|
|
21434
|
+
ok: existsSync10(root),
|
|
21004
21435
|
detail: isRepo(root) ? `${root} (a git repository)` : `${root} (not a git repository)`
|
|
21005
21436
|
});
|
|
21006
21437
|
let writable = false;
|
|
@@ -21133,11 +21564,11 @@ function wordmark(ink = "\u2588") {
|
|
|
21133
21564
|
return rows;
|
|
21134
21565
|
}
|
|
21135
21566
|
var WORDMARK_WIDTH = wordmark()[0].length;
|
|
21136
|
-
function shortPath(path4,
|
|
21137
|
-
if (
|
|
21567
|
+
function shortPath(path4, home2 = process.env.HOME ?? "") {
|
|
21568
|
+
if (home2 && path4 === home2)
|
|
21138
21569
|
return "~";
|
|
21139
|
-
if (
|
|
21140
|
-
return `~${path4.slice(
|
|
21570
|
+
if (home2 && path4.startsWith(`${home2}/`))
|
|
21571
|
+
return `~${path4.slice(home2.length)}`;
|
|
21141
21572
|
return path4;
|
|
21142
21573
|
}
|
|
21143
21574
|
function banner(facts, options = {}) {
|
|
@@ -21301,8 +21732,8 @@ async function chatCommand(context, opening = "") {
|
|
|
21301
21732
|
}
|
|
21302
21733
|
|
|
21303
21734
|
// src/lib/agent/cli/repo-command.ts
|
|
21304
|
-
import { mkdirSync as
|
|
21305
|
-
import { join as
|
|
21735
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, existsSync as existsSync11 } from "fs";
|
|
21736
|
+
import { join as join13 } from "path";
|
|
21306
21737
|
|
|
21307
21738
|
// src/lib/github-repos.ts
|
|
21308
21739
|
var API = "https://api.github.com";
|
|
@@ -21546,11 +21977,11 @@ function filesFromArchive(entries, limits = {}) {
|
|
|
21546
21977
|
// src/lib/agent/cli/repo-command.ts
|
|
21547
21978
|
function workspaceFor(root, owner, name, now) {
|
|
21548
21979
|
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\..*/, "").replace("T", "-");
|
|
21549
|
-
const dir =
|
|
21550
|
-
|
|
21551
|
-
const ignore =
|
|
21552
|
-
if (!
|
|
21553
|
-
|
|
21980
|
+
const dir = join13(root, ".devstation", "repos", `${owner}-${name}-${stamp}`);
|
|
21981
|
+
mkdirSync7(dir, { recursive: true });
|
|
21982
|
+
const ignore = join13(root, ".devstation", ".gitignore");
|
|
21983
|
+
if (!existsSync11(ignore))
|
|
21984
|
+
writeFileSync6(ignore, `*
|
|
21554
21985
|
`);
|
|
21555
21986
|
return dir;
|
|
21556
21987
|
}
|
|
@@ -21726,6 +22157,169 @@ function lineReader(rl, write) {
|
|
|
21726
22157
|
};
|
|
21727
22158
|
}
|
|
21728
22159
|
|
|
22160
|
+
// src/lib/agent/cli/upgrade.ts
|
|
22161
|
+
import { spawnSync } from "child_process";
|
|
22162
|
+
import { createHash as createHash2 } from "crypto";
|
|
22163
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync9, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
22164
|
+
import { dirname as dirname8, join as join14 } from "path";
|
|
22165
|
+
var PACKAGE = "@devstationlabs/cli";
|
|
22166
|
+
var REPO = "linoxbt/dev-shipyard";
|
|
22167
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
22168
|
+
function installMethod(execPath = process.execPath, script = process.argv[1] ?? "") {
|
|
22169
|
+
const slash = (p) => p.replace(/\\/g, "/");
|
|
22170
|
+
const marker = `node_modules/${PACKAGE}/`;
|
|
22171
|
+
if (slash(execPath).includes(marker) || slash(script).includes(marker))
|
|
22172
|
+
return "npm";
|
|
22173
|
+
if (/\.(ts|tsx)$/.test(script) && /(^|\/)bun(\.exe)?$/.test(slash(execPath)))
|
|
22174
|
+
return "source";
|
|
22175
|
+
return "binary";
|
|
22176
|
+
}
|
|
22177
|
+
function compareVersions(a, b) {
|
|
22178
|
+
const parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
22179
|
+
const [x, y] = [parts(a), parts(b)];
|
|
22180
|
+
for (let i = 0;i < Math.max(x.length, y.length); i++) {
|
|
22181
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
22182
|
+
if (d !== 0)
|
|
22183
|
+
return d > 0 ? 1 : -1;
|
|
22184
|
+
}
|
|
22185
|
+
return 0;
|
|
22186
|
+
}
|
|
22187
|
+
async function latestVersion(fetchImpl = fetch, timeoutMs = 4000) {
|
|
22188
|
+
try {
|
|
22189
|
+
const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE.replace("/", "%2f")}/latest`, {
|
|
22190
|
+
headers: { accept: "application/json" },
|
|
22191
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
22192
|
+
});
|
|
22193
|
+
if (!res.ok)
|
|
22194
|
+
return null;
|
|
22195
|
+
const body = await res.json();
|
|
22196
|
+
return typeof body.version === "string" ? body.version : null;
|
|
22197
|
+
} catch {
|
|
22198
|
+
return null;
|
|
22199
|
+
}
|
|
22200
|
+
}
|
|
22201
|
+
function targetFor(platform = process.platform, arch = process.arch) {
|
|
22202
|
+
const map = {
|
|
22203
|
+
"linux-x64": "devstation-linux-x64",
|
|
22204
|
+
"linux-arm64": "devstation-linux-arm64",
|
|
22205
|
+
"darwin-arm64": "devstation-darwin-arm64",
|
|
22206
|
+
"darwin-x64": "devstation-darwin-x64",
|
|
22207
|
+
"win32-x64": "devstation-windows-x64.exe"
|
|
22208
|
+
};
|
|
22209
|
+
return map[`${platform}-${arch}`] ?? null;
|
|
22210
|
+
}
|
|
22211
|
+
function runInherit(command, args) {
|
|
22212
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
22213
|
+
if (result.error)
|
|
22214
|
+
return 127;
|
|
22215
|
+
return result.status ?? 1;
|
|
22216
|
+
}
|
|
22217
|
+
async function upgradeCommand(context, opts = {}) {
|
|
22218
|
+
const t = context.terminal;
|
|
22219
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
22220
|
+
const latest = await latestVersion(fetchImpl);
|
|
22221
|
+
if (!latest) {
|
|
22222
|
+
t.err("Could not reach the npm registry to check for a newer version. Nothing was changed.");
|
|
22223
|
+
return 1;
|
|
22224
|
+
}
|
|
22225
|
+
if (compareVersions(latest, VERSION) <= 0) {
|
|
22226
|
+
t.out(`devstation ${VERSION} is the latest version.`);
|
|
22227
|
+
return 0;
|
|
22228
|
+
}
|
|
22229
|
+
t.out(`devstation ${latest} is available (you have ${VERSION}).`);
|
|
22230
|
+
const method = installMethod(opts.execPath, opts.script);
|
|
22231
|
+
if (opts.check) {
|
|
22232
|
+
t.out(method === "source" ? "This is a source checkout: update it with git pull." : "Upgrade with: devstation upgrade");
|
|
22233
|
+
return 0;
|
|
22234
|
+
}
|
|
22235
|
+
switch (method) {
|
|
22236
|
+
case "npm": {
|
|
22237
|
+
t.out(`Installed through npm, so upgrading through npm: npm install -g ${PACKAGE}@${latest}`);
|
|
22238
|
+
const code = (opts.run ?? runInherit)("npm", ["install", "-g", `${PACKAGE}@${latest}`]);
|
|
22239
|
+
if (code !== 0) {
|
|
22240
|
+
t.err(`npm exited with ${code}. Nothing else was changed. Run it yourself: npm install -g ${PACKAGE}@latest`);
|
|
22241
|
+
return code === 0 ? 1 : code;
|
|
22242
|
+
}
|
|
22243
|
+
t.out(`Upgraded to ${latest}. If your shell still runs the old one, run: hash -r`);
|
|
22244
|
+
return 0;
|
|
22245
|
+
}
|
|
22246
|
+
case "source":
|
|
22247
|
+
t.out("This is running from a source checkout. Update it with git pull, not upgrade.");
|
|
22248
|
+
return 0;
|
|
22249
|
+
case "binary":
|
|
22250
|
+
return replaceBinary(t, latest, opts.execPath ?? process.execPath, fetchImpl);
|
|
22251
|
+
}
|
|
22252
|
+
}
|
|
22253
|
+
async function replaceBinary(t, latest, execPath, fetchImpl) {
|
|
22254
|
+
const target = targetFor();
|
|
22255
|
+
if (!target) {
|
|
22256
|
+
t.err(`There is no prebuilt binary for ${process.platform}-${process.arch}.`);
|
|
22257
|
+
return 1;
|
|
22258
|
+
}
|
|
22259
|
+
if (process.platform === "win32") {
|
|
22260
|
+
t.err(`A running .exe cannot replace itself on Windows. Download ${target} from https://github.com/${REPO}/releases/latest and swap it in.`);
|
|
22261
|
+
return 1;
|
|
22262
|
+
}
|
|
22263
|
+
const base = `https://github.com/${REPO}/releases/download/v${latest}`;
|
|
22264
|
+
t.out(`Downloading ${target} ${latest}\u2026`);
|
|
22265
|
+
try {
|
|
22266
|
+
const sums = await fetchImpl(`${base}/SHA256SUMS`, { redirect: "follow" });
|
|
22267
|
+
if (!sums.ok)
|
|
22268
|
+
throw new Error(`${sums.status} fetching the checksums`);
|
|
22269
|
+
const expected = (await sums.text()).split(`
|
|
22270
|
+
`).map((line) => line.trim().split(/\s+/)).find(([, name]) => name === target)?.[0];
|
|
22271
|
+
if (!expected)
|
|
22272
|
+
throw new Error(`no checksum is published for ${target}`);
|
|
22273
|
+
const bin = await fetchImpl(`${base}/${target}`, { redirect: "follow" });
|
|
22274
|
+
if (!bin.ok)
|
|
22275
|
+
throw new Error(`${bin.status} downloading ${target}`);
|
|
22276
|
+
const body = Buffer.from(await bin.arrayBuffer());
|
|
22277
|
+
const actual = createHash2("sha256").update(body).digest("hex");
|
|
22278
|
+
if (actual !== expected) {
|
|
22279
|
+
throw new Error(`the download does not match its checksum (expected ${expected}, got ${actual})`);
|
|
22280
|
+
}
|
|
22281
|
+
const partial = `${execPath}.partial`;
|
|
22282
|
+
writeFileSync7(partial, body);
|
|
22283
|
+
chmodSync2(partial, 493);
|
|
22284
|
+
renameSync3(partial, execPath);
|
|
22285
|
+
} catch (error2) {
|
|
22286
|
+
const message2 = error2 instanceof Error ? error2.message : String(error2);
|
|
22287
|
+
const permission = /EACCES|EPERM/.test(message2) ? " Run it with sudo, since the binary is in a system directory." : "";
|
|
22288
|
+
t.err(`Upgrade failed and the installed version was left untouched: ${message2}.${permission}`);
|
|
22289
|
+
return 1;
|
|
22290
|
+
}
|
|
22291
|
+
t.out(`Upgraded ${execPath} to ${latest}.`);
|
|
22292
|
+
return 0;
|
|
22293
|
+
}
|
|
22294
|
+
function notifyIfOutdated(terminal, opts = {}) {
|
|
22295
|
+
const env2 = opts.env ?? process.env;
|
|
22296
|
+
if (env2.DEVSTATION_NO_UPDATE_CHECK === "1" || env2.CI)
|
|
22297
|
+
return Promise.resolve();
|
|
22298
|
+
const homeDir = opts.home ?? env2.HOME ?? "";
|
|
22299
|
+
if (!homeDir)
|
|
22300
|
+
return Promise.resolve();
|
|
22301
|
+
const path4 = join14(homeDir, ".devstation", "update-check.json");
|
|
22302
|
+
let cached = {};
|
|
22303
|
+
try {
|
|
22304
|
+
cached = JSON.parse(readFileSync9(path4, "utf8"));
|
|
22305
|
+
} catch {}
|
|
22306
|
+
if (cached.latest && compareVersions(cached.latest, VERSION) > 0) {
|
|
22307
|
+
terminal.err(`devstation ${cached.latest} is available (you have ${VERSION}). Run: devstation upgrade`);
|
|
22308
|
+
}
|
|
22309
|
+
const now = opts.now ?? Date.now();
|
|
22310
|
+
if (cached.checkedAt && now - cached.checkedAt < DAY_MS)
|
|
22311
|
+
return Promise.resolve();
|
|
22312
|
+
return latestVersion(opts.fetchImpl ?? fetch, 2500).then((latest) => {
|
|
22313
|
+
if (!latest)
|
|
22314
|
+
return;
|
|
22315
|
+
try {
|
|
22316
|
+
mkdirSync8(dirname8(path4), { recursive: true, mode: 448 });
|
|
22317
|
+
writeFileSync7(path4, `${JSON.stringify({ checkedAt: now, latest })}
|
|
22318
|
+
`);
|
|
22319
|
+
} catch {}
|
|
22320
|
+
});
|
|
22321
|
+
}
|
|
22322
|
+
|
|
21729
22323
|
// src/lib/agent/cli/index.ts
|
|
21730
22324
|
async function main(argv) {
|
|
21731
22325
|
const parsed = parseArgs(argv);
|
|
@@ -21745,12 +22339,24 @@ ${HELP}`);
|
|
|
21745
22339
|
return 0;
|
|
21746
22340
|
}
|
|
21747
22341
|
const root = resolve4(parsed.root);
|
|
21748
|
-
if (!
|
|
22342
|
+
if (!existsSync12(root)) {
|
|
21749
22343
|
process.stderr.write(`There is no directory at ${root}.
|
|
21750
22344
|
`);
|
|
21751
22345
|
return 2;
|
|
21752
22346
|
}
|
|
21753
|
-
|
|
22347
|
+
let muted = false;
|
|
22348
|
+
const output = new Writable({
|
|
22349
|
+
write(chunk, encoding, done) {
|
|
22350
|
+
if (!muted)
|
|
22351
|
+
process.stdout.write(chunk, encoding);
|
|
22352
|
+
done();
|
|
22353
|
+
}
|
|
22354
|
+
});
|
|
22355
|
+
const readline2 = createInterface2({
|
|
22356
|
+
input: process.stdin,
|
|
22357
|
+
output,
|
|
22358
|
+
terminal: Boolean(process.stdin.isTTY)
|
|
22359
|
+
});
|
|
21754
22360
|
const input = lineReader(readline2, (text) => process.stdout.write(text));
|
|
21755
22361
|
const terminal = {
|
|
21756
22362
|
out: (text) => process.stdout.write(`${text}
|
|
@@ -21758,6 +22364,18 @@ ${HELP}`);
|
|
|
21758
22364
|
err: (text) => process.stderr.write(`${text}
|
|
21759
22365
|
`),
|
|
21760
22366
|
ask: (question) => input.ask(question),
|
|
22367
|
+
askSecret: async (question) => {
|
|
22368
|
+
process.stdout.write(question);
|
|
22369
|
+
muted = true;
|
|
22370
|
+
try {
|
|
22371
|
+
return await input.ask("");
|
|
22372
|
+
} finally {
|
|
22373
|
+
muted = false;
|
|
22374
|
+
if (process.stdin.isTTY)
|
|
22375
|
+
process.stdout.write(`
|
|
22376
|
+
`);
|
|
22377
|
+
}
|
|
22378
|
+
},
|
|
21761
22379
|
write: process.stdout.isTTY ? (text) => process.stdout.write(text) : undefined,
|
|
21762
22380
|
colour: colourEnabled()
|
|
21763
22381
|
};
|
|
@@ -21784,7 +22402,13 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
|
|
|
21784
22402
|
case "tools":
|
|
21785
22403
|
return toolsCommand(offline);
|
|
21786
22404
|
case "config":
|
|
21787
|
-
return configCommand(offline);
|
|
22405
|
+
return parsed.rest.trim() ? configEditCommand(offline, parsed.rest, { project: parsed.project }) : configCommand(offline);
|
|
22406
|
+
case "login":
|
|
22407
|
+
return await loginCommand(offline, parsed.rest);
|
|
22408
|
+
case "logout":
|
|
22409
|
+
return logoutCommand(offline, parsed.rest);
|
|
22410
|
+
case "upgrade":
|
|
22411
|
+
return await upgradeCommand(offline, { check: parsed.check });
|
|
21788
22412
|
case "doctor":
|
|
21789
22413
|
return await doctorCommand(offline);
|
|
21790
22414
|
case "index":
|
|
@@ -21796,12 +22420,19 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
|
|
|
21796
22420
|
default:
|
|
21797
22421
|
break;
|
|
21798
22422
|
}
|
|
21799
|
-
const
|
|
22423
|
+
const settings2 = resolveSettings({ root, model: parsed.model });
|
|
22424
|
+
for (const warning of settings2.warnings)
|
|
22425
|
+
terminal.err(`warning: ${warning}`);
|
|
22426
|
+
const provider = providerFromSettings(settings2);
|
|
21800
22427
|
if (!provider) {
|
|
21801
|
-
terminal.err("No model provider is configured.
|
|
22428
|
+
terminal.err(settings2.problem ?? "No model provider is configured.");
|
|
21802
22429
|
terminal.err(`Run \`${CLI_NAME} doctor\` to see what else is missing.`);
|
|
21803
22430
|
return 2;
|
|
21804
22431
|
}
|
|
22432
|
+
const atHome = homeDirectoryWarning(root);
|
|
22433
|
+
if (atHome)
|
|
22434
|
+
terminal.err(`warning: ${atHome}`);
|
|
22435
|
+
notifyIfOutdated(terminal);
|
|
21805
22436
|
const ctx = context(root, terminal, parsed, provider);
|
|
21806
22437
|
ctx.signal = controller.signal;
|
|
21807
22438
|
if (parsed.command === "repo") {
|
|
@@ -21824,7 +22455,7 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
|
|
|
21824
22455
|
terminal.err(`Give it something to do: ${CLI_NAME} run "fix the failing test in src/lib"`);
|
|
21825
22456
|
return 2;
|
|
21826
22457
|
}
|
|
21827
|
-
terminal.out(`using ${
|
|
22458
|
+
terminal.out(`using ${provider.name}/${provider.model}`);
|
|
21828
22459
|
const { code } = await runCommand(ctx, parsed.rest);
|
|
21829
22460
|
return code;
|
|
21830
22461
|
} finally {
|