@devstationlabs/cli 0.1.2 → 0.1.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 +9 -0
- package/devstation.js +385 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,15 @@ devstation config # what it will use, and where each value came fro
|
|
|
46
46
|
Exporting `ANTHROPIC_API_KEY` or `OPENROUTER_API_KEY` still works, and is what a
|
|
47
47
|
server or CI job should do.
|
|
48
48
|
|
|
49
|
+
## Upgrade
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
devstation upgrade
|
|
53
|
+
```
|
|
54
|
+
|
|
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.
|
|
57
|
+
|
|
49
58
|
## Use it
|
|
50
59
|
|
|
51
60
|
```sh
|
package/devstation.js
CHANGED
|
@@ -12510,7 +12510,7 @@ import { existsSync as existsSync12 } from "fs";
|
|
|
12510
12510
|
|
|
12511
12511
|
// src/lib/agent/cli/args.ts
|
|
12512
12512
|
var CLI_NAME = "devstation";
|
|
12513
|
-
var VERSION = "0.1.
|
|
12513
|
+
var VERSION = "0.1.4";
|
|
12514
12514
|
var COMMANDS = new Set([
|
|
12515
12515
|
"chat",
|
|
12516
12516
|
"run",
|
|
@@ -12528,6 +12528,7 @@ var COMMANDS = new Set([
|
|
|
12528
12528
|
"config",
|
|
12529
12529
|
"login",
|
|
12530
12530
|
"logout",
|
|
12531
|
+
"upgrade",
|
|
12531
12532
|
"doctor",
|
|
12532
12533
|
"version",
|
|
12533
12534
|
"help"
|
|
@@ -12542,6 +12543,7 @@ var OFFLINE_COMMANDS = new Set([
|
|
|
12542
12543
|
"config",
|
|
12543
12544
|
"login",
|
|
12544
12545
|
"logout",
|
|
12546
|
+
"upgrade",
|
|
12545
12547
|
"doctor",
|
|
12546
12548
|
"version",
|
|
12547
12549
|
"help",
|
|
@@ -12558,6 +12560,7 @@ function parseArgs(argv, cwd = process.cwd()) {
|
|
|
12558
12560
|
json: false,
|
|
12559
12561
|
sandbox: (process.env.DEVSTATION_SANDBOX ?? "").toLowerCase() !== "off",
|
|
12560
12562
|
project: false,
|
|
12563
|
+
check: false,
|
|
12561
12564
|
root: cwd
|
|
12562
12565
|
};
|
|
12563
12566
|
const words = [];
|
|
@@ -12585,6 +12588,9 @@ function parseArgs(argv, cwd = process.cwd()) {
|
|
|
12585
12588
|
case "--project":
|
|
12586
12589
|
parsed.project = true;
|
|
12587
12590
|
break;
|
|
12591
|
+
case "--check":
|
|
12592
|
+
parsed.check = true;
|
|
12593
|
+
break;
|
|
12588
12594
|
case "-h":
|
|
12589
12595
|
case "--help":
|
|
12590
12596
|
parsed.command = "help";
|
|
@@ -12675,6 +12681,7 @@ var HELP = `DevStation, the coding agent.
|
|
|
12675
12681
|
${CLI_NAME} tools list the tools it can use, and which ones ask first
|
|
12676
12682
|
${CLI_NAME} login [provider] store an API key and choose a model
|
|
12677
12683
|
${CLI_NAME} logout [provider] remove stored API keys
|
|
12684
|
+
${CLI_NAME} upgrade [--check] update to the latest version (--check only reports)
|
|
12678
12685
|
${CLI_NAME} config show the settings a run would use, and where each came from
|
|
12679
12686
|
${CLI_NAME} config set <key> <value> [--project]
|
|
12680
12687
|
set provider, model or baseUrl
|
|
@@ -12696,6 +12703,7 @@ Options
|
|
|
12696
12703
|
--json JSON from config, sessions, checkpoints and tools
|
|
12697
12704
|
--no-sandbox run commands on this machine instead of in a container
|
|
12698
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
|
|
12699
12707
|
-h, --help this
|
|
12700
12708
|
-v, --version the version
|
|
12701
12709
|
|
|
@@ -12723,7 +12731,7 @@ var SESSION_HELP = ` /undo rewind the last checkpoint
|
|
|
12723
12731
|
import { accessSync, constants as constants2, existsSync as existsSync10 } from "fs";
|
|
12724
12732
|
|
|
12725
12733
|
// src/lib/agent/git.ts
|
|
12726
|
-
import { existsSync } from "fs";
|
|
12734
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
12727
12735
|
import { join } from "path";
|
|
12728
12736
|
|
|
12729
12737
|
// src/lib/agent/shell.ts
|
|
@@ -12941,7 +12949,16 @@ function cloneTargetProblem(target) {
|
|
|
12941
12949
|
// src/lib/agent/git.ts
|
|
12942
12950
|
var CHECKPOINT_PREFIX = "agent checkpoint:";
|
|
12943
12951
|
function isRepo(root) {
|
|
12944
|
-
|
|
12952
|
+
const dotGit = join(root, ".git");
|
|
12953
|
+
if (!existsSync(dotGit))
|
|
12954
|
+
return false;
|
|
12955
|
+
try {
|
|
12956
|
+
if (statSync(dotGit).isFile())
|
|
12957
|
+
return readFileSync(dotGit, "utf8").startsWith("gitdir:");
|
|
12958
|
+
} catch {
|
|
12959
|
+
return false;
|
|
12960
|
+
}
|
|
12961
|
+
return existsSync(join(dotGit, "HEAD"));
|
|
12945
12962
|
}
|
|
12946
12963
|
function quote(value) {
|
|
12947
12964
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -13066,7 +13083,7 @@ import {
|
|
|
13066
13083
|
cpSync,
|
|
13067
13084
|
existsSync as existsSync3,
|
|
13068
13085
|
mkdirSync as mkdirSync2,
|
|
13069
|
-
readFileSync as
|
|
13086
|
+
readFileSync as readFileSync3,
|
|
13070
13087
|
readdirSync as readdirSync2,
|
|
13071
13088
|
rmSync,
|
|
13072
13089
|
writeFileSync as writeFileSync2
|
|
@@ -13077,10 +13094,10 @@ import { dirname as dirname2, join as join3 } from "path";
|
|
|
13077
13094
|
import {
|
|
13078
13095
|
existsSync as existsSync2,
|
|
13079
13096
|
mkdirSync,
|
|
13080
|
-
readFileSync,
|
|
13097
|
+
readFileSync as readFileSync2,
|
|
13081
13098
|
readdirSync,
|
|
13082
13099
|
realpathSync,
|
|
13083
|
-
statSync,
|
|
13100
|
+
statSync as statSync2,
|
|
13084
13101
|
writeFileSync
|
|
13085
13102
|
} from "fs";
|
|
13086
13103
|
import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
@@ -13157,10 +13174,10 @@ class Workspace {
|
|
|
13157
13174
|
if (!existsSync2(resolved.absolute)) {
|
|
13158
13175
|
return { ok: false, reason: `There is no file at ${relativePath}.` };
|
|
13159
13176
|
}
|
|
13160
|
-
if (
|
|
13177
|
+
if (statSync2(resolved.absolute).isDirectory()) {
|
|
13161
13178
|
return { ok: false, reason: `${relativePath} is a directory, not a file.` };
|
|
13162
13179
|
}
|
|
13163
|
-
const buffer =
|
|
13180
|
+
const buffer = readFileSync2(resolved.absolute);
|
|
13164
13181
|
if (looksBinary(buffer)) {
|
|
13165
13182
|
return {
|
|
13166
13183
|
ok: false,
|
|
@@ -13180,10 +13197,10 @@ class Workspace {
|
|
|
13180
13197
|
if (!resolved.ok)
|
|
13181
13198
|
return { ok: false, reason: resolved.reason };
|
|
13182
13199
|
if (existsSync2(resolved.absolute)) {
|
|
13183
|
-
if (
|
|
13200
|
+
if (statSync2(resolved.absolute).isDirectory()) {
|
|
13184
13201
|
return { ok: false, reason: `${relativePath} is a directory.` };
|
|
13185
13202
|
}
|
|
13186
|
-
if (looksBinary(
|
|
13203
|
+
if (looksBinary(readFileSync2(resolved.absolute))) {
|
|
13187
13204
|
return { ok: false, reason: `${relativePath} is a binary file and was not overwritten.` };
|
|
13188
13205
|
}
|
|
13189
13206
|
}
|
|
@@ -13195,7 +13212,7 @@ class Workspace {
|
|
|
13195
13212
|
const resolved = this.resolve(relativePath);
|
|
13196
13213
|
return resolved.ok && existsSync2(resolved.absolute);
|
|
13197
13214
|
}
|
|
13198
|
-
list(subdir = ".") {
|
|
13215
|
+
list(subdir = ".", limit = Number.POSITIVE_INFINITY) {
|
|
13199
13216
|
const base = subdir === "." || subdir === "" ? { ok: true, absolute: this.root } : this.resolve(subdir);
|
|
13200
13217
|
if (!base.ok || !existsSync2(base.absolute))
|
|
13201
13218
|
return [];
|
|
@@ -13204,6 +13221,8 @@ class Workspace {
|
|
|
13204
13221
|
const out = [];
|
|
13205
13222
|
const walk = (dir) => {
|
|
13206
13223
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
13224
|
+
if (out.length >= limit)
|
|
13225
|
+
return;
|
|
13207
13226
|
if (entry.name.startsWith(".") && skip.has(entry.name))
|
|
13208
13227
|
continue;
|
|
13209
13228
|
if (skip.has(entry.name))
|
|
@@ -13236,7 +13255,7 @@ function listSnapshots(root) {
|
|
|
13236
13255
|
if (!existsSync3(manifest))
|
|
13237
13256
|
continue;
|
|
13238
13257
|
try {
|
|
13239
|
-
found.push(JSON.parse(
|
|
13258
|
+
found.push(JSON.parse(readFileSync3(manifest, "utf8")));
|
|
13240
13259
|
} catch {}
|
|
13241
13260
|
}
|
|
13242
13261
|
return found.sort((a, b) => b.id.localeCompare(a.id));
|
|
@@ -18114,9 +18133,9 @@ import {
|
|
|
18114
18133
|
chmodSync,
|
|
18115
18134
|
existsSync as existsSync4,
|
|
18116
18135
|
mkdirSync as mkdirSync3,
|
|
18117
|
-
readFileSync as
|
|
18136
|
+
readFileSync as readFileSync4,
|
|
18118
18137
|
renameSync,
|
|
18119
|
-
statSync as
|
|
18138
|
+
statSync as statSync3,
|
|
18120
18139
|
writeFileSync as writeFileSync3
|
|
18121
18140
|
} from "fs";
|
|
18122
18141
|
import { dirname as dirname4, join as join5 } from "path";
|
|
@@ -18151,11 +18170,12 @@ function readSettingsFile(path4, problems = []) {
|
|
|
18151
18170
|
if (!existsSync4(path4))
|
|
18152
18171
|
return {};
|
|
18153
18172
|
try {
|
|
18154
|
-
const raw = JSON.parse(
|
|
18173
|
+
const raw = JSON.parse(readFileSync4(path4, "utf8"));
|
|
18155
18174
|
const out = {};
|
|
18156
18175
|
if (raw.provider !== undefined) {
|
|
18157
|
-
|
|
18158
|
-
|
|
18176
|
+
const named = typeof raw.provider === "string" ? raw.provider.toLowerCase() : raw.provider;
|
|
18177
|
+
if (isProvider(named))
|
|
18178
|
+
out.provider = named;
|
|
18159
18179
|
else
|
|
18160
18180
|
problems.push(`${path4}: unknown provider "${String(raw.provider)}".`);
|
|
18161
18181
|
}
|
|
@@ -18195,11 +18215,11 @@ function readCredentials(home, problems = []) {
|
|
|
18195
18215
|
if (!existsSync4(path4))
|
|
18196
18216
|
return {};
|
|
18197
18217
|
try {
|
|
18198
|
-
const mode =
|
|
18218
|
+
const mode = statSync3(path4).mode & 511;
|
|
18199
18219
|
if (mode & 63) {
|
|
18200
18220
|
problems.push(`${path4} is readable by other users (mode ${mode.toString(8)}). Fix it: chmod 600 ${path4}`);
|
|
18201
18221
|
}
|
|
18202
|
-
const raw = JSON.parse(
|
|
18222
|
+
const raw = JSON.parse(readFileSync4(path4, "utf8"));
|
|
18203
18223
|
const out = {};
|
|
18204
18224
|
for (const id of PROVIDER_IDS) {
|
|
18205
18225
|
if (typeof raw[id] === "string" && raw[id].trim())
|
|
@@ -18228,8 +18248,9 @@ function resolveSettings(opts) {
|
|
|
18228
18248
|
let provider = null;
|
|
18229
18249
|
let providerSource = "not set";
|
|
18230
18250
|
if (env2.DEVSTATION_PROVIDER) {
|
|
18231
|
-
|
|
18232
|
-
|
|
18251
|
+
const named = env2.DEVSTATION_PROVIDER.toLowerCase();
|
|
18252
|
+
if (isProvider(named)) {
|
|
18253
|
+
provider = named;
|
|
18233
18254
|
providerSource = "DEVSTATION_PROVIDER";
|
|
18234
18255
|
} else {
|
|
18235
18256
|
warnings.push(`DEVSTATION_PROVIDER="${env2.DEVSTATION_PROVIDER}" is not a provider, so it was ignored.`);
|
|
@@ -18446,7 +18467,7 @@ async function embedMissing(store, provider, options = {}) {
|
|
|
18446
18467
|
import { join as join7 } from "path";
|
|
18447
18468
|
|
|
18448
18469
|
// src/lib/agent/repo-session.ts
|
|
18449
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
18470
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
|
|
18450
18471
|
import { join as join6, relative as relative2, sep as sep2 } from "path";
|
|
18451
18472
|
var SKIP_DIRS = new Set([
|
|
18452
18473
|
".git",
|
|
@@ -18490,9 +18511,9 @@ function readWorkspace(root, maxFileBytes = 1024 * 1024) {
|
|
|
18490
18511
|
}
|
|
18491
18512
|
if (!item.isFile())
|
|
18492
18513
|
continue;
|
|
18493
|
-
if (
|
|
18514
|
+
if (statSync4(full).size > maxFileBytes)
|
|
18494
18515
|
continue;
|
|
18495
|
-
const buffer =
|
|
18516
|
+
const buffer = readFileSync5(full);
|
|
18496
18517
|
if (looksBinary(buffer))
|
|
18497
18518
|
continue;
|
|
18498
18519
|
files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
|
|
@@ -18501,6 +18522,69 @@ function readWorkspace(root, maxFileBytes = 1024 * 1024) {
|
|
|
18501
18522
|
walk3(root);
|
|
18502
18523
|
return files;
|
|
18503
18524
|
}
|
|
18525
|
+
var INDEX_SKIP_DIRS = new Set([
|
|
18526
|
+
...SKIP_DIRS,
|
|
18527
|
+
"target",
|
|
18528
|
+
"coverage",
|
|
18529
|
+
"__pycache__",
|
|
18530
|
+
".venv",
|
|
18531
|
+
"venv",
|
|
18532
|
+
".gradle",
|
|
18533
|
+
"out"
|
|
18534
|
+
]);
|
|
18535
|
+
var HIDDEN_KEEP = new Set([".github"]);
|
|
18536
|
+
function readWorkspaceBounded(root, opts = {}) {
|
|
18537
|
+
const maxFiles = opts.maxFiles ?? 20000;
|
|
18538
|
+
const until = Date.now() + (opts.deadlineMs ?? 1e4);
|
|
18539
|
+
const maxBytes = opts.maxFileBytes ?? 1024 * 1024;
|
|
18540
|
+
const files = {};
|
|
18541
|
+
let seen = 0;
|
|
18542
|
+
let stopped = null;
|
|
18543
|
+
const walk3 = (dir) => {
|
|
18544
|
+
let entries;
|
|
18545
|
+
try {
|
|
18546
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
18547
|
+
} catch {
|
|
18548
|
+
return;
|
|
18549
|
+
}
|
|
18550
|
+
for (const item of entries) {
|
|
18551
|
+
if (stopped)
|
|
18552
|
+
return;
|
|
18553
|
+
if (item.isSymbolicLink())
|
|
18554
|
+
continue;
|
|
18555
|
+
const full = join6(dir, item.name);
|
|
18556
|
+
if (item.isDirectory()) {
|
|
18557
|
+
if (INDEX_SKIP_DIRS.has(item.name))
|
|
18558
|
+
continue;
|
|
18559
|
+
if (item.name.startsWith(".") && !HIDDEN_KEEP.has(item.name))
|
|
18560
|
+
continue;
|
|
18561
|
+
walk3(full);
|
|
18562
|
+
continue;
|
|
18563
|
+
}
|
|
18564
|
+
if (!item.isFile())
|
|
18565
|
+
continue;
|
|
18566
|
+
if (seen >= maxFiles) {
|
|
18567
|
+
stopped = "max-files";
|
|
18568
|
+
return;
|
|
18569
|
+
}
|
|
18570
|
+
if ((seen & 63) === 0 && Date.now() > until) {
|
|
18571
|
+
stopped = "deadline";
|
|
18572
|
+
return;
|
|
18573
|
+
}
|
|
18574
|
+
seen++;
|
|
18575
|
+
try {
|
|
18576
|
+
if (statSync4(full).size > maxBytes)
|
|
18577
|
+
continue;
|
|
18578
|
+
const buffer = readFileSync5(full);
|
|
18579
|
+
if (looksBinary(buffer))
|
|
18580
|
+
continue;
|
|
18581
|
+
files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
|
|
18582
|
+
} catch {}
|
|
18583
|
+
}
|
|
18584
|
+
};
|
|
18585
|
+
walk3(root);
|
|
18586
|
+
return { files, stopped };
|
|
18587
|
+
}
|
|
18504
18588
|
var MAX_TITLE = 72;
|
|
18505
18589
|
function proposalFor(goal, result, files) {
|
|
18506
18590
|
const firstLine = (goal || result.summary).split(`
|
|
@@ -18869,7 +18953,7 @@ class MemoryStore {
|
|
|
18869
18953
|
this.db.run("DELETE FROM chunks WHERE path = ?", [path4]);
|
|
18870
18954
|
this.db.run("DELETE FROM files WHERE path = ?", [path4]);
|
|
18871
18955
|
}
|
|
18872
|
-
reindex(files) {
|
|
18956
|
+
reindex(files, options = {}) {
|
|
18873
18957
|
const result = { scanned: 0, reindexed: 0, removed: 0, chunks: 0 };
|
|
18874
18958
|
this.db.run("BEGIN");
|
|
18875
18959
|
try {
|
|
@@ -18882,9 +18966,11 @@ class MemoryStore {
|
|
|
18882
18966
|
result.chunks += this.replaceFile(path4, content);
|
|
18883
18967
|
result.reindexed++;
|
|
18884
18968
|
}
|
|
18885
|
-
|
|
18886
|
-
|
|
18887
|
-
|
|
18969
|
+
if (!options.partial) {
|
|
18970
|
+
for (const gone of known) {
|
|
18971
|
+
this.removeFile(gone);
|
|
18972
|
+
result.removed++;
|
|
18973
|
+
}
|
|
18888
18974
|
}
|
|
18889
18975
|
this.db.run("COMMIT");
|
|
18890
18976
|
} catch (error2) {
|
|
@@ -18969,8 +19055,27 @@ function openStore(root) {
|
|
|
18969
19055
|
return new MemoryStore(storePath(root));
|
|
18970
19056
|
}
|
|
18971
19057
|
async function indexWorkspace(root, options = {}) {
|
|
19058
|
+
const started = Date.now();
|
|
19059
|
+
const home = options.home ?? process.env.HOME ?? "";
|
|
19060
|
+
const strip2 = (p) => p.replace(/\/+$/, "");
|
|
19061
|
+
if (home && strip2(root) === strip2(home)) {
|
|
19062
|
+
return {
|
|
19063
|
+
scanned: 0,
|
|
19064
|
+
reindexed: 0,
|
|
19065
|
+
removed: 0,
|
|
19066
|
+
chunks: 0,
|
|
19067
|
+
embedded: 0,
|
|
19068
|
+
embeddingModel: null,
|
|
19069
|
+
stopped: "home",
|
|
19070
|
+
ms: 0
|
|
19071
|
+
};
|
|
19072
|
+
}
|
|
18972
19073
|
const store = options.store ?? openStore(root);
|
|
18973
|
-
const
|
|
19074
|
+
const read = readWorkspaceBounded(root, {
|
|
19075
|
+
maxFiles: options.maxFiles,
|
|
19076
|
+
deadlineMs: options.deadlineMs
|
|
19077
|
+
});
|
|
19078
|
+
const result = store.reindex(read.files, { partial: read.stopped !== null });
|
|
18974
19079
|
let embedded = 0;
|
|
18975
19080
|
if (options.embeddings) {
|
|
18976
19081
|
try {
|
|
@@ -18979,11 +19084,17 @@ async function indexWorkspace(root, options = {}) {
|
|
|
18979
19084
|
embedded = 0;
|
|
18980
19085
|
}
|
|
18981
19086
|
}
|
|
18982
|
-
return {
|
|
19087
|
+
return {
|
|
19088
|
+
...result,
|
|
19089
|
+
embedded,
|
|
19090
|
+
embeddingModel: options.embeddings?.model ?? null,
|
|
19091
|
+
stopped: read.stopped,
|
|
19092
|
+
ms: Date.now() - started
|
|
19093
|
+
};
|
|
18983
19094
|
}
|
|
18984
19095
|
|
|
18985
19096
|
// src/lib/agent/memory/project-memory.ts
|
|
18986
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as
|
|
19097
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
18987
19098
|
import { dirname as dirname6, join as join8 } from "path";
|
|
18988
19099
|
var DEFAULT_FILE = "PROJECT_MEMORY.md";
|
|
18989
19100
|
var HEADER = `# Project memory
|
|
@@ -19002,7 +19113,7 @@ function readMemory(root) {
|
|
|
19002
19113
|
const path4 = memoryPath(root);
|
|
19003
19114
|
if (!existsSync5(path4))
|
|
19004
19115
|
return [];
|
|
19005
|
-
return parseMemory(
|
|
19116
|
+
return parseMemory(readFileSync6(path4, "utf8"));
|
|
19006
19117
|
}
|
|
19007
19118
|
var ENTRY = /^- \[([^\]]+)\](?:\s*\(([^)]*)\))?\s+([\s\S]*)$/;
|
|
19008
19119
|
function parseMemory(text) {
|
|
@@ -19026,7 +19137,7 @@ function remember(root, note, tag = null, now = new Date) {
|
|
|
19026
19137
|
if (!text) {
|
|
19027
19138
|
return { ok: false, path: path4, message: "There was nothing to remember." };
|
|
19028
19139
|
}
|
|
19029
|
-
const existing = existsSync5(path4) ?
|
|
19140
|
+
const existing = existsSync5(path4) ? readFileSync6(path4, "utf8") : "";
|
|
19030
19141
|
const entries = parseMemory(existing);
|
|
19031
19142
|
const normal = (value) => value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
19032
19143
|
if (entries.some((entry) => normal(entry.note) === normal(text))) {
|
|
@@ -19062,7 +19173,7 @@ function renderMemory(entries) {
|
|
|
19062
19173
|
|
|
19063
19174
|
// src/lib/agent/mcp.ts
|
|
19064
19175
|
import { spawn as spawn3 } from "child_process";
|
|
19065
|
-
import { existsSync as existsSync6, readFileSync as
|
|
19176
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
19066
19177
|
import { join as join9 } from "path";
|
|
19067
19178
|
function configPaths(root, home = process.env.HOME ?? "") {
|
|
19068
19179
|
return [
|
|
@@ -19077,7 +19188,7 @@ function loadConfig(root, home = process.env.HOME ?? "") {
|
|
|
19077
19188
|
if (!existsSync6(path4))
|
|
19078
19189
|
continue;
|
|
19079
19190
|
try {
|
|
19080
|
-
const parsed = JSON.parse(
|
|
19191
|
+
const parsed = JSON.parse(readFileSync7(path4, "utf8"));
|
|
19081
19192
|
for (const [name, server] of Object.entries(parsed.mcpServers ?? {})) {
|
|
19082
19193
|
if (server && typeof server.command === "string")
|
|
19083
19194
|
merged.mcpServers[name] = server;
|
|
@@ -19337,11 +19448,11 @@ function failedResult(message) {
|
|
|
19337
19448
|
|
|
19338
19449
|
// src/lib/agent/sandbox-exec.ts
|
|
19339
19450
|
import { randomBytes } from "crypto";
|
|
19340
|
-
import { existsSync as existsSync8, statSync as
|
|
19451
|
+
import { existsSync as existsSync8, statSync as statSync6, unlinkSync } from "fs";
|
|
19341
19452
|
import { isAbsolute as isAbsolute3, join as join11, relative as relative4, resolve as resolve3, sep as sep4 } from "path";
|
|
19342
19453
|
|
|
19343
19454
|
// src/lib/agent/project.ts
|
|
19344
|
-
import { existsSync as existsSync7, readFileSync as
|
|
19455
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
19345
19456
|
import { dirname as dirname7, join as join10, relative as relative3, sep as sep3 } from "path";
|
|
19346
19457
|
var SKIP = new Set([
|
|
19347
19458
|
"node_modules",
|
|
@@ -19368,7 +19479,7 @@ var MANIFESTS = [
|
|
|
19368
19479
|
var MAX_DEPTH = 2;
|
|
19369
19480
|
function readScripts(absolute) {
|
|
19370
19481
|
try {
|
|
19371
|
-
const parsed = JSON.parse(
|
|
19482
|
+
const parsed = JSON.parse(readFileSync8(absolute, "utf8"));
|
|
19372
19483
|
return parsed.scripts ?? {};
|
|
19373
19484
|
} catch {
|
|
19374
19485
|
return {};
|
|
@@ -19401,7 +19512,7 @@ function detectManifests(root) {
|
|
|
19401
19512
|
continue;
|
|
19402
19513
|
const child = join10(dir, entry);
|
|
19403
19514
|
try {
|
|
19404
|
-
if (
|
|
19515
|
+
if (statSync5(child).isDirectory())
|
|
19405
19516
|
scan(child, depth + 1);
|
|
19406
19517
|
} catch {}
|
|
19407
19518
|
}
|
|
@@ -19542,7 +19653,7 @@ var NEEDS = {
|
|
|
19542
19653
|
go: ["go"]
|
|
19543
19654
|
};
|
|
19544
19655
|
async function probeImage(workspace, image, runtime) {
|
|
19545
|
-
const ecosystems =
|
|
19656
|
+
const ecosystems = gatingEcosystems(detectManifests(workspace));
|
|
19546
19657
|
const wanted = [...new Set(ecosystems.flatMap((e) => NEEDS[e]))];
|
|
19547
19658
|
if (wanted.length === 0)
|
|
19548
19659
|
return { ok: true, missing: [], ecosystems };
|
|
@@ -19554,10 +19665,13 @@ async function probeImage(workspace, image, runtime) {
|
|
|
19554
19665
|
`).map((l) => l.trim()).filter(Boolean);
|
|
19555
19666
|
return { ok: missing.length === 0, missing, ecosystems };
|
|
19556
19667
|
}
|
|
19668
|
+
function gatingEcosystems(manifests) {
|
|
19669
|
+
return [...new Set(manifests.filter((m) => m.dir === "").map((m) => m.ecosystem))];
|
|
19670
|
+
}
|
|
19557
19671
|
function probeProblem(probe, image) {
|
|
19558
19672
|
if (probe.ok)
|
|
19559
19673
|
return null;
|
|
19560
|
-
return `
|
|
19674
|
+
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.";
|
|
19561
19675
|
}
|
|
19562
19676
|
function quote2(value) {
|
|
19563
19677
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -19672,7 +19786,7 @@ function sandboxExecutor(options) {
|
|
|
19672
19786
|
return "The sandbox could not write to the workspace. Check the mount and try --no-sandbox.";
|
|
19673
19787
|
}
|
|
19674
19788
|
try {
|
|
19675
|
-
const stat2 =
|
|
19789
|
+
const stat2 = statSync6(path4);
|
|
19676
19790
|
const [uid, gid] = user.split(":").map(Number);
|
|
19677
19791
|
if (stat2.uid !== uid || stat2.gid !== gid) {
|
|
19678
19792
|
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.";
|
|
@@ -19940,8 +20054,20 @@ function fail(reason) {
|
|
|
19940
20054
|
function executeFileTool(workspace, name, args) {
|
|
19941
20055
|
switch (name) {
|
|
19942
20056
|
case "list_files": {
|
|
19943
|
-
const
|
|
19944
|
-
|
|
20057
|
+
const LIMIT = 2000;
|
|
20058
|
+
const files = workspace.list(String(args.path ?? "."), LIMIT + 1);
|
|
20059
|
+
if (files.length === 0)
|
|
20060
|
+
return { ok: true, output: "The workspace is empty." };
|
|
20061
|
+
if (files.length > LIMIT) {
|
|
20062
|
+
return {
|
|
20063
|
+
ok: true,
|
|
20064
|
+
output: `${files.slice(0, LIMIT).join(`
|
|
20065
|
+
`)}
|
|
20066
|
+
|
|
20067
|
+
` + `(Stopped at ${LIMIT} files: this directory is very large. List a subdirectory, or search for what you need.)`
|
|
20068
|
+
};
|
|
20069
|
+
}
|
|
20070
|
+
return { ok: true, output: files.join(`
|
|
19945
20071
|
`) };
|
|
19946
20072
|
}
|
|
19947
20073
|
case "read_file": {
|
|
@@ -19978,7 +20104,10 @@ function executeFileTool(workspace, name, args) {
|
|
|
19978
20104
|
return fail("No search query was given.");
|
|
19979
20105
|
const hits = [];
|
|
19980
20106
|
let truncated = false;
|
|
19981
|
-
|
|
20107
|
+
const MAX_SCAN = 20000;
|
|
20108
|
+
const scanned = workspace.list(".", MAX_SCAN + 1);
|
|
20109
|
+
const scanLimited = scanned.length > MAX_SCAN;
|
|
20110
|
+
for (const path4 of scanned.slice(0, MAX_SCAN)) {
|
|
19982
20111
|
const file = workspace.read(path4);
|
|
19983
20112
|
if (!file.ok)
|
|
19984
20113
|
continue;
|
|
@@ -19997,14 +20126,18 @@ function executeFileTool(workspace, name, args) {
|
|
|
19997
20126
|
if (truncated)
|
|
19998
20127
|
break;
|
|
19999
20128
|
}
|
|
20000
|
-
|
|
20001
|
-
|
|
20129
|
+
const scanNote = scanLimited ? `
|
|
20130
|
+
|
|
20131
|
+
(Searched the first ${MAX_SCAN} files only: this workspace is very large. Search a subdirectory.)` : "";
|
|
20132
|
+
if (hits.length === 0) {
|
|
20133
|
+
return { ok: true, output: `No match for "${args.query}".${scanNote}` };
|
|
20134
|
+
}
|
|
20002
20135
|
return {
|
|
20003
20136
|
ok: true,
|
|
20004
|
-
output: truncated ? `${hits.join(`
|
|
20137
|
+
output: `${truncated ? `${hits.join(`
|
|
20005
20138
|
`)}
|
|
20006
20139
|
\u2026 more matches not shown` : hits.join(`
|
|
20007
|
-
`)
|
|
20140
|
+
`)}${scanNote}`
|
|
20008
20141
|
};
|
|
20009
20142
|
}
|
|
20010
20143
|
default:
|
|
@@ -20725,10 +20858,10 @@ import {
|
|
|
20725
20858
|
appendFileSync,
|
|
20726
20859
|
existsSync as existsSync9,
|
|
20727
20860
|
mkdirSync as mkdirSync6,
|
|
20728
|
-
readFileSync as
|
|
20861
|
+
readFileSync as readFileSync9,
|
|
20729
20862
|
readdirSync as readdirSync6,
|
|
20730
20863
|
renameSync as renameSync2,
|
|
20731
|
-
statSync as
|
|
20864
|
+
statSync as statSync7,
|
|
20732
20865
|
writeFileSync as writeFileSync5
|
|
20733
20866
|
} from "fs";
|
|
20734
20867
|
import { join as join12 } from "path";
|
|
@@ -20791,7 +20924,7 @@ class SessionStore {
|
|
|
20791
20924
|
if (!existsSync9(path4))
|
|
20792
20925
|
return null;
|
|
20793
20926
|
try {
|
|
20794
|
-
return JSON.parse(
|
|
20927
|
+
return JSON.parse(readFileSync9(path4, "utf8"));
|
|
20795
20928
|
} catch {
|
|
20796
20929
|
return null;
|
|
20797
20930
|
}
|
|
@@ -20813,10 +20946,10 @@ class SessionStore {
|
|
|
20813
20946
|
const path4 = this.eventPath(id);
|
|
20814
20947
|
if (!existsSync9(path4))
|
|
20815
20948
|
return { events: [], offset: 0 };
|
|
20816
|
-
const size =
|
|
20949
|
+
const size = statSync7(path4).size;
|
|
20817
20950
|
if (size <= fromByte)
|
|
20818
20951
|
return { events: [], offset: size };
|
|
20819
|
-
const text =
|
|
20952
|
+
const text = readFileSync9(path4, "utf8").slice(fromByte);
|
|
20820
20953
|
const events = [];
|
|
20821
20954
|
let consumed = 0;
|
|
20822
20955
|
for (const line of text.split(`
|
|
@@ -20911,7 +21044,8 @@ function renderSessions(records) {
|
|
|
20911
21044
|
}
|
|
20912
21045
|
|
|
20913
21046
|
// src/lib/agent/cli/commands.ts
|
|
20914
|
-
async function buildExecutor(root, sandbox) {
|
|
21047
|
+
async function buildExecutor(root, sandbox, warn = (message) => process.stderr.write(`warning: ${message}
|
|
21048
|
+
`)) {
|
|
20915
21049
|
if (!sandbox)
|
|
20916
21050
|
return { executor: hostExecutor() };
|
|
20917
21051
|
const readiness = await sandboxReadiness();
|
|
@@ -20921,7 +21055,7 @@ async function buildExecutor(root, sandbox) {
|
|
|
20921
21055
|
const probe = await probeImage(root, readiness.imageName, readiness.runtimeName);
|
|
20922
21056
|
const mismatch = probeProblem(probe, readiness.imageName);
|
|
20923
21057
|
if (mismatch)
|
|
20924
|
-
|
|
21058
|
+
warn(mismatch);
|
|
20925
21059
|
return { executor: sandboxExecutor({ workspace: root }) };
|
|
20926
21060
|
}
|
|
20927
21061
|
function isYes(answer) {
|
|
@@ -20959,9 +21093,21 @@ async function runCommand(context, goal, options = {}) {
|
|
|
20959
21093
|
executor = built.executor;
|
|
20960
21094
|
}
|
|
20961
21095
|
const embeddings = embeddingsFromEnv();
|
|
21096
|
+
const firstIndex = !existsSync10(storePath(context.root));
|
|
21097
|
+
if (firstIndex && !options.quiet)
|
|
21098
|
+
terminal.out("Indexing the workspace so the agent can search it\u2026");
|
|
20962
21099
|
const memory = openStore(context.root);
|
|
20963
21100
|
try {
|
|
20964
|
-
await indexWorkspace(context.root, { store: memory, embeddings });
|
|
21101
|
+
const indexed = await indexWorkspace(context.root, { store: memory, embeddings });
|
|
21102
|
+
if (!options.quiet) {
|
|
21103
|
+
if (indexed.stopped === "home") {
|
|
21104
|
+
terminal.err("Not indexing your home directory. The agent can still read and list files; cd into a project for a real index.");
|
|
21105
|
+
} else if (indexed.stopped) {
|
|
21106
|
+
terminal.err(`Indexed ${indexed.scanned} files and stopped at the ${indexed.stopped === "deadline" ? "time" : "file"} limit: this workspace is very large. cd into the project you mean.`);
|
|
21107
|
+
} else if (firstIndex || indexed.ms > 1500) {
|
|
21108
|
+
terminal.out(`Indexed ${indexed.scanned} files in ${(indexed.ms / 1000).toFixed(1)}s.`);
|
|
21109
|
+
}
|
|
21110
|
+
}
|
|
20965
21111
|
} catch {}
|
|
20966
21112
|
const mcp = await McpHub.start(loadConfig(context.root));
|
|
20967
21113
|
for (const status of mcp.status) {
|
|
@@ -21254,6 +21400,14 @@ ${r.problem}`);
|
|
|
21254
21400
|
function home() {
|
|
21255
21401
|
return process.env.HOME ?? "";
|
|
21256
21402
|
}
|
|
21403
|
+
function homeDirectoryWarning(root, homeDir = process.env.HOME ?? "") {
|
|
21404
|
+
if (!homeDir)
|
|
21405
|
+
return null;
|
|
21406
|
+
const strip2 = (p) => p.replace(/\/+$/, "");
|
|
21407
|
+
if (strip2(root) !== strip2(homeDir))
|
|
21408
|
+
return null;
|
|
21409
|
+
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.";
|
|
21410
|
+
}
|
|
21257
21411
|
function configEditCommand(context, rest, opts = {}) {
|
|
21258
21412
|
const [action, key, ...valueWords] = rest.trim().split(/\s+/);
|
|
21259
21413
|
const value = valueWords.join(" ").trim();
|
|
@@ -21289,7 +21443,7 @@ function configEditCommand(context, rest, opts = {}) {
|
|
|
21289
21443
|
context.terminal.err(`Give it a value: devstation config set ${name} <value>`);
|
|
21290
21444
|
return 2;
|
|
21291
21445
|
}
|
|
21292
|
-
if (name === "provider" && !PROVIDER_IDS.includes(value)) {
|
|
21446
|
+
if (name === "provider" && !PROVIDER_IDS.includes(value.toLowerCase())) {
|
|
21293
21447
|
context.terminal.err(`Unknown provider "${value}". Providers: ${PROVIDER_IDS.join(", ")}.`);
|
|
21294
21448
|
return 2;
|
|
21295
21449
|
}
|
|
@@ -21297,7 +21451,7 @@ function configEditCommand(context, rest, opts = {}) {
|
|
|
21297
21451
|
context.terminal.err("baseUrl must start with http:// or https://.");
|
|
21298
21452
|
return 2;
|
|
21299
21453
|
}
|
|
21300
|
-
current[name] = name === "baseUrl" ? value.replace(/\/+$/, "") : value;
|
|
21454
|
+
current[name] = name === "baseUrl" ? value.replace(/\/+$/, "") : name === "provider" ? value.toLowerCase() : value;
|
|
21301
21455
|
writeSettingsFile(path4, current);
|
|
21302
21456
|
context.terminal.out(`Set ${name} = ${current[name]} in ${path4}.`);
|
|
21303
21457
|
return 0;
|
|
@@ -21308,13 +21462,13 @@ function configEditCommand(context, rest, opts = {}) {
|
|
|
21308
21462
|
async function loginCommand(context, rest) {
|
|
21309
21463
|
const t = context.terminal;
|
|
21310
21464
|
const secret = t.askSecret ? (q) => t.askSecret(q) : (q) => t.ask(q);
|
|
21311
|
-
let provider = rest.trim().split(/\s+/)[0];
|
|
21465
|
+
let provider = (rest.trim().split(/\s+/)[0] ?? "").toLowerCase();
|
|
21312
21466
|
if (!provider) {
|
|
21313
21467
|
t.out("Which provider?");
|
|
21314
21468
|
t.out(" anthropic Claude, directly (prompt caching, native tool use)");
|
|
21315
21469
|
t.out(" openrouter one key for Claude, GPT, Gemini, DeepSeek and more");
|
|
21316
21470
|
t.out(" openai OpenAI, or any compatible server: Ollama, LM Studio, Groq, Together");
|
|
21317
|
-
provider = (await t.ask("provider [anthropic]: ")).trim() || "anthropic";
|
|
21471
|
+
provider = (await t.ask("provider [anthropic]: ")).trim().toLowerCase() || "anthropic";
|
|
21318
21472
|
}
|
|
21319
21473
|
if (!PROVIDER_IDS.includes(provider)) {
|
|
21320
21474
|
t.err(`Unknown provider "${provider}". Providers: ${PROVIDER_IDS.join(", ")}.`);
|
|
@@ -21372,7 +21526,7 @@ async function loginCommand(context, rest) {
|
|
|
21372
21526
|
}
|
|
21373
21527
|
function logoutCommand(context, rest) {
|
|
21374
21528
|
const h = home();
|
|
21375
|
-
const target = rest.trim().split(/\s+/)[0];
|
|
21529
|
+
const target = (rest.trim().split(/\s+/)[0] ?? "").toLowerCase();
|
|
21376
21530
|
if (target && !PROVIDER_IDS.includes(target)) {
|
|
21377
21531
|
context.terminal.err(`Unknown provider "${target}". Providers: ${PROVIDER_IDS.join(", ")}.`);
|
|
21378
21532
|
return 2;
|
|
@@ -22135,6 +22289,169 @@ function lineReader(rl, write) {
|
|
|
22135
22289
|
};
|
|
22136
22290
|
}
|
|
22137
22291
|
|
|
22292
|
+
// src/lib/agent/cli/upgrade.ts
|
|
22293
|
+
import { spawnSync } from "child_process";
|
|
22294
|
+
import { createHash as createHash2 } from "crypto";
|
|
22295
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
22296
|
+
import { dirname as dirname8, join as join14 } from "path";
|
|
22297
|
+
var PACKAGE = "@devstationlabs/cli";
|
|
22298
|
+
var REPO = "linoxbt/dev-shipyard";
|
|
22299
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
22300
|
+
function installMethod(execPath = process.execPath, script = process.argv[1] ?? "") {
|
|
22301
|
+
const slash = (p) => p.replace(/\\/g, "/");
|
|
22302
|
+
const marker = `node_modules/${PACKAGE}/`;
|
|
22303
|
+
if (slash(execPath).includes(marker) || slash(script).includes(marker))
|
|
22304
|
+
return "npm";
|
|
22305
|
+
if (/\.(ts|tsx)$/.test(script) && /(^|\/)bun(\.exe)?$/.test(slash(execPath)))
|
|
22306
|
+
return "source";
|
|
22307
|
+
return "binary";
|
|
22308
|
+
}
|
|
22309
|
+
function compareVersions(a, b) {
|
|
22310
|
+
const parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
22311
|
+
const [x, y] = [parts(a), parts(b)];
|
|
22312
|
+
for (let i = 0;i < Math.max(x.length, y.length); i++) {
|
|
22313
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
22314
|
+
if (d !== 0)
|
|
22315
|
+
return d > 0 ? 1 : -1;
|
|
22316
|
+
}
|
|
22317
|
+
return 0;
|
|
22318
|
+
}
|
|
22319
|
+
async function latestVersion(fetchImpl = fetch, timeoutMs = 4000) {
|
|
22320
|
+
try {
|
|
22321
|
+
const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE.replace("/", "%2f")}/latest`, {
|
|
22322
|
+
headers: { accept: "application/json" },
|
|
22323
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
22324
|
+
});
|
|
22325
|
+
if (!res.ok)
|
|
22326
|
+
return null;
|
|
22327
|
+
const body = await res.json();
|
|
22328
|
+
return typeof body.version === "string" ? body.version : null;
|
|
22329
|
+
} catch {
|
|
22330
|
+
return null;
|
|
22331
|
+
}
|
|
22332
|
+
}
|
|
22333
|
+
function targetFor(platform = process.platform, arch = process.arch) {
|
|
22334
|
+
const map = {
|
|
22335
|
+
"linux-x64": "devstation-linux-x64",
|
|
22336
|
+
"linux-arm64": "devstation-linux-arm64",
|
|
22337
|
+
"darwin-arm64": "devstation-darwin-arm64",
|
|
22338
|
+
"darwin-x64": "devstation-darwin-x64",
|
|
22339
|
+
"win32-x64": "devstation-windows-x64.exe"
|
|
22340
|
+
};
|
|
22341
|
+
return map[`${platform}-${arch}`] ?? null;
|
|
22342
|
+
}
|
|
22343
|
+
function runInherit(command, args) {
|
|
22344
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
22345
|
+
if (result.error)
|
|
22346
|
+
return 127;
|
|
22347
|
+
return result.status ?? 1;
|
|
22348
|
+
}
|
|
22349
|
+
async function upgradeCommand(context, opts = {}) {
|
|
22350
|
+
const t = context.terminal;
|
|
22351
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
22352
|
+
const latest = await latestVersion(fetchImpl);
|
|
22353
|
+
if (!latest) {
|
|
22354
|
+
t.err("Could not reach the npm registry to check for a newer version. Nothing was changed.");
|
|
22355
|
+
return 1;
|
|
22356
|
+
}
|
|
22357
|
+
if (compareVersions(latest, VERSION) <= 0) {
|
|
22358
|
+
t.out(`devstation ${VERSION} is the latest version.`);
|
|
22359
|
+
return 0;
|
|
22360
|
+
}
|
|
22361
|
+
t.out(`devstation ${latest} is available (you have ${VERSION}).`);
|
|
22362
|
+
const method = installMethod(opts.execPath, opts.script);
|
|
22363
|
+
if (opts.check) {
|
|
22364
|
+
t.out(method === "source" ? "This is a source checkout: update it with git pull." : "Upgrade with: devstation upgrade");
|
|
22365
|
+
return 0;
|
|
22366
|
+
}
|
|
22367
|
+
switch (method) {
|
|
22368
|
+
case "npm": {
|
|
22369
|
+
t.out(`Installed through npm, so upgrading through npm: npm install -g ${PACKAGE}@${latest}`);
|
|
22370
|
+
const code = (opts.run ?? runInherit)("npm", ["install", "-g", `${PACKAGE}@${latest}`]);
|
|
22371
|
+
if (code !== 0) {
|
|
22372
|
+
t.err(`npm exited with ${code}. Nothing else was changed. Run it yourself: npm install -g ${PACKAGE}@latest`);
|
|
22373
|
+
return code === 0 ? 1 : code;
|
|
22374
|
+
}
|
|
22375
|
+
t.out(`Upgraded to ${latest}. If your shell still runs the old one, run: hash -r`);
|
|
22376
|
+
return 0;
|
|
22377
|
+
}
|
|
22378
|
+
case "source":
|
|
22379
|
+
t.out("This is running from a source checkout. Update it with git pull, not upgrade.");
|
|
22380
|
+
return 0;
|
|
22381
|
+
case "binary":
|
|
22382
|
+
return replaceBinary(t, latest, opts.execPath ?? process.execPath, fetchImpl);
|
|
22383
|
+
}
|
|
22384
|
+
}
|
|
22385
|
+
async function replaceBinary(t, latest, execPath, fetchImpl) {
|
|
22386
|
+
const target = targetFor();
|
|
22387
|
+
if (!target) {
|
|
22388
|
+
t.err(`There is no prebuilt binary for ${process.platform}-${process.arch}.`);
|
|
22389
|
+
return 1;
|
|
22390
|
+
}
|
|
22391
|
+
if (process.platform === "win32") {
|
|
22392
|
+
t.err(`A running .exe cannot replace itself on Windows. Download ${target} from https://github.com/${REPO}/releases/latest and swap it in.`);
|
|
22393
|
+
return 1;
|
|
22394
|
+
}
|
|
22395
|
+
const base = `https://github.com/${REPO}/releases/download/v${latest}`;
|
|
22396
|
+
t.out(`Downloading ${target} ${latest}\u2026`);
|
|
22397
|
+
try {
|
|
22398
|
+
const sums = await fetchImpl(`${base}/SHA256SUMS`, { redirect: "follow" });
|
|
22399
|
+
if (!sums.ok)
|
|
22400
|
+
throw new Error(`${sums.status} fetching the checksums`);
|
|
22401
|
+
const expected = (await sums.text()).split(`
|
|
22402
|
+
`).map((line) => line.trim().split(/\s+/)).find(([, name]) => name === target)?.[0];
|
|
22403
|
+
if (!expected)
|
|
22404
|
+
throw new Error(`no checksum is published for ${target}`);
|
|
22405
|
+
const bin = await fetchImpl(`${base}/${target}`, { redirect: "follow" });
|
|
22406
|
+
if (!bin.ok)
|
|
22407
|
+
throw new Error(`${bin.status} downloading ${target}`);
|
|
22408
|
+
const body = Buffer.from(await bin.arrayBuffer());
|
|
22409
|
+
const actual = createHash2("sha256").update(body).digest("hex");
|
|
22410
|
+
if (actual !== expected) {
|
|
22411
|
+
throw new Error(`the download does not match its checksum (expected ${expected}, got ${actual})`);
|
|
22412
|
+
}
|
|
22413
|
+
const partial = `${execPath}.partial`;
|
|
22414
|
+
writeFileSync7(partial, body);
|
|
22415
|
+
chmodSync2(partial, 493);
|
|
22416
|
+
renameSync3(partial, execPath);
|
|
22417
|
+
} catch (error2) {
|
|
22418
|
+
const message2 = error2 instanceof Error ? error2.message : String(error2);
|
|
22419
|
+
const permission = /EACCES|EPERM/.test(message2) ? " Run it with sudo, since the binary is in a system directory." : "";
|
|
22420
|
+
t.err(`Upgrade failed and the installed version was left untouched: ${message2}.${permission}`);
|
|
22421
|
+
return 1;
|
|
22422
|
+
}
|
|
22423
|
+
t.out(`Upgraded ${execPath} to ${latest}.`);
|
|
22424
|
+
return 0;
|
|
22425
|
+
}
|
|
22426
|
+
function notifyIfOutdated(terminal, opts = {}) {
|
|
22427
|
+
const env2 = opts.env ?? process.env;
|
|
22428
|
+
if (env2.DEVSTATION_NO_UPDATE_CHECK === "1" || env2.CI)
|
|
22429
|
+
return Promise.resolve();
|
|
22430
|
+
const homeDir = opts.home ?? env2.HOME ?? "";
|
|
22431
|
+
if (!homeDir)
|
|
22432
|
+
return Promise.resolve();
|
|
22433
|
+
const path4 = join14(homeDir, ".devstation", "update-check.json");
|
|
22434
|
+
let cached = {};
|
|
22435
|
+
try {
|
|
22436
|
+
cached = JSON.parse(readFileSync10(path4, "utf8"));
|
|
22437
|
+
} catch {}
|
|
22438
|
+
if (cached.latest && compareVersions(cached.latest, VERSION) > 0) {
|
|
22439
|
+
terminal.err(`devstation ${cached.latest} is available (you have ${VERSION}). Run: devstation upgrade`);
|
|
22440
|
+
}
|
|
22441
|
+
const now = opts.now ?? Date.now();
|
|
22442
|
+
if (cached.checkedAt && now - cached.checkedAt < DAY_MS)
|
|
22443
|
+
return Promise.resolve();
|
|
22444
|
+
return latestVersion(opts.fetchImpl ?? fetch, 2500).then((latest) => {
|
|
22445
|
+
if (!latest)
|
|
22446
|
+
return;
|
|
22447
|
+
try {
|
|
22448
|
+
mkdirSync8(dirname8(path4), { recursive: true, mode: 448 });
|
|
22449
|
+
writeFileSync7(path4, `${JSON.stringify({ checkedAt: now, latest })}
|
|
22450
|
+
`);
|
|
22451
|
+
} catch {}
|
|
22452
|
+
});
|
|
22453
|
+
}
|
|
22454
|
+
|
|
22138
22455
|
// src/lib/agent/cli/index.ts
|
|
22139
22456
|
async function main(argv) {
|
|
22140
22457
|
const parsed = parseArgs(argv);
|
|
@@ -22222,6 +22539,8 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
|
|
|
22222
22539
|
return await loginCommand(offline, parsed.rest);
|
|
22223
22540
|
case "logout":
|
|
22224
22541
|
return logoutCommand(offline, parsed.rest);
|
|
22542
|
+
case "upgrade":
|
|
22543
|
+
return await upgradeCommand(offline, { check: parsed.check });
|
|
22225
22544
|
case "doctor":
|
|
22226
22545
|
return await doctorCommand(offline);
|
|
22227
22546
|
case "index":
|
|
@@ -22242,6 +22561,10 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
|
|
|
22242
22561
|
terminal.err(`Run \`${CLI_NAME} doctor\` to see what else is missing.`);
|
|
22243
22562
|
return 2;
|
|
22244
22563
|
}
|
|
22564
|
+
const atHome = homeDirectoryWarning(root);
|
|
22565
|
+
if (atHome)
|
|
22566
|
+
terminal.err(`warning: ${atHome}`);
|
|
22567
|
+
notifyIfOutdated(terminal);
|
|
22245
22568
|
const ctx = context(root, terminal, parsed, provider);
|
|
22246
22569
|
ctx.signal = controller.signal;
|
|
22247
22570
|
if (parsed.command === "repo") {
|