@lucasschirm/devinp 0.1.10 → 0.1.13
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 +57 -12
- package/bin/command.js +186 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,18 +24,48 @@ The package ships as a self-contained bundle — no dependencies are installed.
|
|
|
24
24
|
not authenticated, `devinp` exits with an error (use `--source` for a saved
|
|
25
25
|
snapshot).
|
|
26
26
|
|
|
27
|
-
### When `devin` fails or truncates
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
### When `devin` fails, pages, or truncates
|
|
28
|
+
|
|
29
|
+
`devin` is an interactive CLI, and on some machines it stays interactive even
|
|
30
|
+
when a script calls it: the model list comes back paged ("click to show more")
|
|
31
|
+
and the JSON never reaches stdout, so `devinp` used to die with *"returned
|
|
32
|
+
invalid JSON (… 0 bytes)"*. It now works harder to get the whole document:
|
|
33
|
+
|
|
34
|
+
- Prices are captured through a temp file in `os.tmpdir()` — a pipe stream from
|
|
35
|
+
`devin` can be truncated mid-write, so `devinp` points `devin`'s stdout at a
|
|
36
|
+
real file first. The temp file is always removed after the run.
|
|
37
|
+
- **Retries that take the terminal away from `devin`.** If an attempt yields no
|
|
38
|
+
usable JSON, `devinp` runs `devin` again, each time stripping more of the
|
|
39
|
+
interactive terminal off it:
|
|
40
|
+
|
|
41
|
+
| # | stdout goes to | environment | controlling terminal |
|
|
42
|
+
| - | ------------ | -------------------------------------------------------- | --------------------- |
|
|
43
|
+
| 1 | temp file | yours, untouched | yes — the healthy path, one `devin` call |
|
|
44
|
+
| 2 | temp file | `PAGER=cat`, `GIT_PAGER=cat`, `TERM=dumb`, `NO_COLOR=1`, `CI=1` | **none** |
|
|
45
|
+
| 3 | pipe | same | **none** |
|
|
46
|
+
|
|
47
|
+
Spawned in its own session, `devin` cannot open `/dev/tty`: a pager or a TUI
|
|
48
|
+
that paints a "click to show more" view onto the terminal has nowhere to
|
|
49
|
+
draw, so the document has to come out of stdout. Nothing lands on the screen
|
|
50
|
+
you are typing in, and Ctrl-C there no longer kills the lookup. The first
|
|
51
|
+
attempt that parses wins, so the healthy path is unchanged.
|
|
52
|
+
- Noise around the document is tolerated: ANSI colours, an update banner in
|
|
53
|
+
front, or the `pass a slug to --model` hint behind are stripped before the
|
|
54
|
+
JSON is parsed.
|
|
33
55
|
- If `devin`'s output is cut off mid-stream, `devinp` repairs the document,
|
|
34
56
|
prints a warning, and renders the partial table.
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
- A real CLI failure (not signed in, network down) is reported as-is instead of
|
|
58
|
+
being retried.
|
|
59
|
+
- If nothing worked, the error lists all three attempts — bytes, exit code,
|
|
60
|
+
stderr, timeout — and how to feed the data in yourself. That always works,
|
|
61
|
+
because a redirect or a pipe makes `devin` write the whole document in one go:
|
|
62
|
+
(and if it does not, `command -v devin` tells you whether devinp is even
|
|
63
|
+
talking to the CLI and not to a shell function or agent wrapper around it).
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
devin models list --format json > models.json && devinp --source models.json
|
|
67
|
+
devin models list --format json | devinp --source=-
|
|
68
|
+
```
|
|
39
69
|
|
|
40
70
|
## Usage
|
|
41
71
|
|
|
@@ -55,6 +85,7 @@ devinp --web # serve an interactive page on http://127.0.0.1:432
|
|
|
55
85
|
devinp --web --port 8080 # pick a port
|
|
56
86
|
|
|
57
87
|
devinp --source models.json # read a saved `devin models list --format json`
|
|
88
|
+
devin models list --format json | devinp --source=- # feed it straight from stdin
|
|
58
89
|
devinp --json # emit normalized rows as JSON
|
|
59
90
|
```
|
|
60
91
|
|
|
@@ -106,11 +137,25 @@ so `npm publish`, git-URL installs, and `npm link` always ship a fresh bundle.
|
|
|
106
137
|
|
|
107
138
|
## Publishing
|
|
108
139
|
|
|
109
|
-
`.github/workflows/publish.yml`
|
|
110
|
-
[npm trusted publishing](https://docs.npmjs.
|
|
140
|
+
`.github/workflows/publish.yml` publishes a patch release on every push to main
|
|
141
|
+
with [npm trusted publishing](https://docs.npmjs.org/trusted-publishers) (OIDC —
|
|
111
142
|
no `NPM_TOKEN`). Requires the trusted publisher to be configured on npmjs.com:
|
|
112
143
|
repository `lucasschirm/devinp`, workflow `publish.yml`.
|
|
113
144
|
|
|
145
|
+
It **never commits to main** — the branch ruleset requires every change to
|
|
146
|
+
arrive through a pull request, and a rejected release commit used to strand the
|
|
147
|
+
publish step that ran after it (GH013, then `npm publish` skipped). The released
|
|
148
|
+
version therefore lives in the npm registry and in the git tag, not in main's
|
|
149
|
+
`package.json`:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
git ls-remote --tags origin # released versions
|
|
153
|
+
npm view @lucasschirm/devinp version # latest published
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The workflow takes the first patch number that is free in both places, so a tag
|
|
157
|
+
left behind by an older run cannot wedge it.
|
|
158
|
+
|
|
114
159
|
## License
|
|
115
160
|
|
|
116
161
|
MIT
|
package/bin/command.js
CHANGED
|
@@ -4193,14 +4193,14 @@ var require_emoji_regex = __commonJS({
|
|
|
4193
4193
|
var require_string_width = __commonJS({
|
|
4194
4194
|
"node_modules/string-width/index.js"(exports, module) {
|
|
4195
4195
|
"use strict";
|
|
4196
|
-
var
|
|
4196
|
+
var stripAnsi2 = require_strip_ansi();
|
|
4197
4197
|
var isFullwidthCodePoint = require_is_fullwidth_code_point();
|
|
4198
4198
|
var emojiRegex = require_emoji_regex();
|
|
4199
4199
|
var stringWidth = (string) => {
|
|
4200
4200
|
if (typeof string !== "string" || string.length === 0) {
|
|
4201
4201
|
return 0;
|
|
4202
4202
|
}
|
|
4203
|
-
string =
|
|
4203
|
+
string = stripAnsi2(string);
|
|
4204
4204
|
if (string.length === 0) {
|
|
4205
4205
|
return 0;
|
|
4206
4206
|
}
|
|
@@ -5332,7 +5332,7 @@ var require_wrap_ansi = __commonJS({
|
|
|
5332
5332
|
"node_modules/wrap-ansi/index.js"(exports, module) {
|
|
5333
5333
|
"use strict";
|
|
5334
5334
|
var stringWidth = require_string_width();
|
|
5335
|
-
var
|
|
5335
|
+
var stripAnsi2 = require_strip_ansi();
|
|
5336
5336
|
var ansiStyles = require_ansi_styles();
|
|
5337
5337
|
var ESCAPES = /* @__PURE__ */ new Set([
|
|
5338
5338
|
"\x1B",
|
|
@@ -5351,7 +5351,7 @@ var require_wrap_ansi = __commonJS({
|
|
|
5351
5351
|
const characters = [...word];
|
|
5352
5352
|
let isInsideEscape = false;
|
|
5353
5353
|
let isInsideLinkEscape = false;
|
|
5354
|
-
let visible2 = stringWidth(
|
|
5354
|
+
let visible2 = stringWidth(stripAnsi2(rows[rows.length - 1]));
|
|
5355
5355
|
for (const [index, character] of characters.entries()) {
|
|
5356
5356
|
const characterLength = stringWidth(character);
|
|
5357
5357
|
if (visible2 + characterLength <= columns) {
|
|
@@ -13355,7 +13355,7 @@ var require_parse2 = __commonJS({
|
|
|
13355
13355
|
const parts = [];
|
|
13356
13356
|
let bracket = 0;
|
|
13357
13357
|
let paren = 0;
|
|
13358
|
-
let
|
|
13358
|
+
let quote2 = 0;
|
|
13359
13359
|
let value = "";
|
|
13360
13360
|
let escaped = false;
|
|
13361
13361
|
for (const ch of input) {
|
|
@@ -13370,11 +13370,11 @@ var require_parse2 = __commonJS({
|
|
|
13370
13370
|
continue;
|
|
13371
13371
|
}
|
|
13372
13372
|
if (ch === '"') {
|
|
13373
|
-
|
|
13373
|
+
quote2 = quote2 === 1 ? 0 : 1;
|
|
13374
13374
|
value += ch;
|
|
13375
13375
|
continue;
|
|
13376
13376
|
}
|
|
13377
|
-
if (
|
|
13377
|
+
if (quote2 === 0) {
|
|
13378
13378
|
if (ch === "[") {
|
|
13379
13379
|
bracket++;
|
|
13380
13380
|
} else if (ch === "]" && bracket > 0) {
|
|
@@ -13451,7 +13451,7 @@ var require_parse2 = __commonJS({
|
|
|
13451
13451
|
}
|
|
13452
13452
|
let bracket = 0;
|
|
13453
13453
|
let paren = 0;
|
|
13454
|
-
let
|
|
13454
|
+
let quote2 = 0;
|
|
13455
13455
|
let escaped = false;
|
|
13456
13456
|
for (let i = 1; i < pattern.length; i++) {
|
|
13457
13457
|
const ch = pattern[i];
|
|
@@ -13464,10 +13464,10 @@ var require_parse2 = __commonJS({
|
|
|
13464
13464
|
continue;
|
|
13465
13465
|
}
|
|
13466
13466
|
if (ch === '"') {
|
|
13467
|
-
|
|
13467
|
+
quote2 = quote2 === 1 ? 0 : 1;
|
|
13468
13468
|
continue;
|
|
13469
13469
|
}
|
|
13470
|
-
if (
|
|
13470
|
+
if (quote2 === 1) {
|
|
13471
13471
|
continue;
|
|
13472
13472
|
}
|
|
13473
13473
|
if (ch === "[") {
|
|
@@ -17633,7 +17633,7 @@ var require_parse3 = __commonJS({
|
|
|
17633
17633
|
debug = () => {
|
|
17634
17634
|
};
|
|
17635
17635
|
}
|
|
17636
|
-
var
|
|
17636
|
+
var readStdin2 = async () => {
|
|
17637
17637
|
const { stdin, stdout } = process;
|
|
17638
17638
|
if (stdin.isTTY)
|
|
17639
17639
|
return null;
|
|
@@ -17669,7 +17669,7 @@ var require_parse3 = __commonJS({
|
|
|
17669
17669
|
}, { once: true });
|
|
17670
17670
|
});
|
|
17671
17671
|
};
|
|
17672
|
-
exports.readStdin =
|
|
17672
|
+
exports.readStdin = readStdin2;
|
|
17673
17673
|
function isNegativeNumber(input) {
|
|
17674
17674
|
return /^-\d/g.test(input);
|
|
17675
17675
|
}
|
|
@@ -41394,15 +41394,15 @@ var require_quote = __commonJS({
|
|
|
41394
41394
|
'"': '\\"',
|
|
41395
41395
|
"\\": "\\\\"
|
|
41396
41396
|
};
|
|
41397
|
-
function
|
|
41397
|
+
function quote2(s) {
|
|
41398
41398
|
rxEscapable.lastIndex = 0;
|
|
41399
41399
|
return '"' + (rxEscapable.test(s) ? s.replace(rxEscapable, (a2) => {
|
|
41400
41400
|
const c = escaped[a2];
|
|
41401
41401
|
return typeof c === "string" ? c : "\\u" + ("0000" + a2.charCodeAt(0).toString(16)).slice(-4);
|
|
41402
41402
|
}) : s) + '"';
|
|
41403
41403
|
}
|
|
41404
|
-
exports.default =
|
|
41405
|
-
|
|
41404
|
+
exports.default = quote2;
|
|
41405
|
+
quote2.code = 'require("ajv/dist/runtime/quote").default';
|
|
41406
41406
|
}
|
|
41407
41407
|
});
|
|
41408
41408
|
|
|
@@ -55088,45 +55088,115 @@ function scanJson(s, end) {
|
|
|
55088
55088
|
|
|
55089
55089
|
// src/lib/source.js
|
|
55090
55090
|
var DEVIN_TIMEOUT_MS = 3e4;
|
|
55091
|
-
|
|
55091
|
+
var NON_INTERACTIVE_ENV = {
|
|
55092
|
+
PAGER: "cat",
|
|
55093
|
+
GIT_PAGER: "cat",
|
|
55094
|
+
TERM: "dumb",
|
|
55095
|
+
NO_COLOR: "1",
|
|
55096
|
+
CI: "1"
|
|
55097
|
+
};
|
|
55098
|
+
var DETACHED_SPAWN = { detached: true, windowsHide: true };
|
|
55099
|
+
var STRATEGIES = [
|
|
55100
|
+
{ label: "temp file", file: true, env: null },
|
|
55101
|
+
{ label: "temp file, paging disabled, no controlling terminal", file: true, env: NON_INTERACTIVE_ENV, detached: true },
|
|
55102
|
+
{ label: "pipe, paging disabled, no controlling terminal", file: false, env: NON_INTERACTIVE_ENV, detached: true }
|
|
55103
|
+
];
|
|
55104
|
+
var ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][\s\S]*?(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g;
|
|
55105
|
+
function stripAnsi(text) {
|
|
55106
|
+
return text.replace(ANSI, "");
|
|
55107
|
+
}
|
|
55108
|
+
function parseJsonLoosely(text) {
|
|
55109
|
+
const clean = stripAnsi(text);
|
|
55110
|
+
try {
|
|
55111
|
+
return JSON.parse(clean);
|
|
55112
|
+
} catch {
|
|
55113
|
+
}
|
|
55114
|
+
const start = clean.indexOf("{");
|
|
55115
|
+
const end = clean.lastIndexOf("}");
|
|
55116
|
+
if (start >= 0 && end > start) {
|
|
55117
|
+
try {
|
|
55118
|
+
return JSON.parse(clean.slice(start, end + 1));
|
|
55119
|
+
} catch {
|
|
55120
|
+
}
|
|
55121
|
+
}
|
|
55122
|
+
return null;
|
|
55123
|
+
}
|
|
55124
|
+
function spawnDevin({ command, env, stdout, timeoutMs, detached = false }) {
|
|
55125
|
+
return new Promise((resolve, reject) => {
|
|
55126
|
+
const child = spawn(command, ["models", "list", "--format", "json"], {
|
|
55127
|
+
stdio: ["ignore", stdout, "pipe"],
|
|
55128
|
+
...env ? { env: { ...process.env, ...env } } : {},
|
|
55129
|
+
...detached ? DETACHED_SPAWN : {}
|
|
55130
|
+
});
|
|
55131
|
+
let stdoutText = "";
|
|
55132
|
+
let stderr = "";
|
|
55133
|
+
let timedOut = false;
|
|
55134
|
+
const timer = setTimeout(() => {
|
|
55135
|
+
timedOut = true;
|
|
55136
|
+
child.kill("SIGKILL");
|
|
55137
|
+
}, timeoutMs);
|
|
55138
|
+
child.stdout?.on("data", (chunk) => {
|
|
55139
|
+
stdoutText += chunk;
|
|
55140
|
+
});
|
|
55141
|
+
child.stderr.on("data", (chunk) => {
|
|
55142
|
+
stderr += chunk;
|
|
55143
|
+
});
|
|
55144
|
+
child.on("error", (error) => {
|
|
55145
|
+
clearTimeout(timer);
|
|
55146
|
+
reject(error);
|
|
55147
|
+
});
|
|
55148
|
+
child.on("close", (code) => {
|
|
55149
|
+
clearTimeout(timer);
|
|
55150
|
+
resolve({ stdout: stdoutText, stderr, code, timedOut });
|
|
55151
|
+
});
|
|
55152
|
+
});
|
|
55153
|
+
}
|
|
55154
|
+
async function captureToFile({ command, env, timeoutMs, detached }) {
|
|
55092
55155
|
const file = join2(tmpdir(), `devinp-models-${process.pid}-${Date.now()}.json`);
|
|
55093
55156
|
const handle = await open(file, "w");
|
|
55094
55157
|
try {
|
|
55095
|
-
const {
|
|
55096
|
-
|
|
55097
|
-
|
|
55098
|
-
});
|
|
55099
|
-
let stderr2 = "";
|
|
55100
|
-
let timedOut2 = false;
|
|
55101
|
-
const timer = setTimeout(() => {
|
|
55102
|
-
timedOut2 = true;
|
|
55103
|
-
child.kill("SIGKILL");
|
|
55104
|
-
}, timeoutMs);
|
|
55105
|
-
child.stderr.on("data", (chunk) => {
|
|
55106
|
-
stderr2 += chunk;
|
|
55107
|
-
});
|
|
55108
|
-
child.on("error", (error) => {
|
|
55109
|
-
clearTimeout(timer);
|
|
55110
|
-
reject(error);
|
|
55111
|
-
});
|
|
55112
|
-
child.on("close", (childCode) => {
|
|
55113
|
-
clearTimeout(timer);
|
|
55114
|
-
resolve({ code: childCode, stderr: stderr2, timedOut: timedOut2 });
|
|
55115
|
-
});
|
|
55116
|
-
});
|
|
55117
|
-
if (timedOut) {
|
|
55118
|
-
throw new Error(`\`devin models list\` timed out after ${timeoutMs / 1e3}s`);
|
|
55119
|
-
}
|
|
55120
|
-
if (code !== 0) {
|
|
55121
|
-
throw new Error(`\`devin models list\` failed \u2014 ${stderr.trim() || `exited with code ${code}`}`);
|
|
55122
|
-
}
|
|
55123
|
-
return await readFile2(file, "utf8");
|
|
55158
|
+
const result = await spawnDevin({ command, env, stdout: handle.fd, timeoutMs, detached });
|
|
55159
|
+
result.stdout = await readFile2(file, "utf8");
|
|
55160
|
+
return result;
|
|
55124
55161
|
} finally {
|
|
55125
55162
|
await handle.close();
|
|
55126
55163
|
await unlink(file).catch(() => {
|
|
55127
55164
|
});
|
|
55128
55165
|
}
|
|
55129
55166
|
}
|
|
55167
|
+
async function runDevinJson({ command = "devin", timeoutMs = DEVIN_TIMEOUT_MS } = {}) {
|
|
55168
|
+
const attempts = [];
|
|
55169
|
+
for (const [index, strategy] of STRATEGIES.entries()) {
|
|
55170
|
+
let result;
|
|
55171
|
+
try {
|
|
55172
|
+
result = strategy.file ? await captureToFile({ command, env: strategy.env, timeoutMs, detached: strategy.detached }) : await spawnDevin({ command, env: strategy.env, stdout: "pipe", timeoutMs, detached: strategy.detached });
|
|
55173
|
+
} catch (error) {
|
|
55174
|
+
if (error.code === "ENOENT") throw error;
|
|
55175
|
+
attempts.push({ label: strategy.label, bytes: 0, code: null, stderr: "", timedOut: false, note: error.message });
|
|
55176
|
+
continue;
|
|
55177
|
+
}
|
|
55178
|
+
const raw = result.stdout ?? "";
|
|
55179
|
+
const parsed = raw.trim() ? parseJsonLoosely(raw) : null;
|
|
55180
|
+
attempts.push({
|
|
55181
|
+
label: strategy.label,
|
|
55182
|
+
bytes: raw.length,
|
|
55183
|
+
code: result.code,
|
|
55184
|
+
stderr: result.stderr.trim(),
|
|
55185
|
+
timedOut: result.timedOut,
|
|
55186
|
+
raw
|
|
55187
|
+
});
|
|
55188
|
+
if (parsed != null) return { raw, parsed, attempts, timeoutMs };
|
|
55189
|
+
if (index === 0 && result.code !== 0 && !result.timedOut && result.stderr.trim()) {
|
|
55190
|
+
throw new Error(`\`devin models list\` failed \u2014 ${result.stderr.trim()}`);
|
|
55191
|
+
}
|
|
55192
|
+
}
|
|
55193
|
+
const explained = [...attempts].reverse().find((a2) => a2.stderr);
|
|
55194
|
+
if (explained && attempts.every((a2) => a2.timedOut || a2.code !== 0)) {
|
|
55195
|
+
throw new Error(`\`devin models list\` failed \u2014 ${explained.stderr}`);
|
|
55196
|
+
}
|
|
55197
|
+
const longest = attempts.reduce((acc, a2) => (a2.raw?.length ?? 0) > acc.length ? a2.raw : acc, "");
|
|
55198
|
+
return { raw: longest || "", parsed: null, attempts, timeoutMs };
|
|
55199
|
+
}
|
|
55130
55200
|
function assertFamilies(data, sourceName) {
|
|
55131
55201
|
if (data == null || typeof data !== "object" || !Array.isArray(data.families)) {
|
|
55132
55202
|
throw new Error(`Unable to get model prices: ${sourceName} has no "families" array (expected \`devin models list --format json\` output)`);
|
|
@@ -55136,43 +55206,87 @@ function assertFamilies(data, sourceName) {
|
|
|
55136
55206
|
}
|
|
55137
55207
|
return data;
|
|
55138
55208
|
}
|
|
55139
|
-
|
|
55209
|
+
function quote(text) {
|
|
55210
|
+
return JSON.stringify(text.length > 200 ? `\u2026${text.slice(-199)}` : text);
|
|
55211
|
+
}
|
|
55212
|
+
function describeAttempt(attempt, timeoutMs) {
|
|
55213
|
+
const parts = [`${attempt.label}: ${attempt.bytes} bytes`];
|
|
55214
|
+
if (attempt.timedOut) parts.push(`timed out after ${timeoutMs / 1e3}s`);
|
|
55215
|
+
else parts.push(`exit ${attempt.code}`);
|
|
55216
|
+
if (attempt.stderr) parts.push(`stderr ${quote(attempt.stderr)}`);
|
|
55217
|
+
if (attempt.note) parts.push(attempt.note);
|
|
55218
|
+
return parts.join(", ");
|
|
55219
|
+
}
|
|
55220
|
+
function describeFailure(attempts, timeoutMs) {
|
|
55221
|
+
const bytes = Math.max(...attempts.map((a2) => a2.bytes), 0);
|
|
55222
|
+
const longest = attempts.find((a2) => a2.bytes === bytes);
|
|
55223
|
+
const tail = longest?.raw?.trimEnd().slice(-200) ?? "";
|
|
55224
|
+
const lines = [];
|
|
55225
|
+
if (bytes === 0) {
|
|
55226
|
+
lines.push(
|
|
55227
|
+
'Unable to get model prices: `devin models list --format json` exited without writing the JSON to stdout (0 bytes). devinp re-ran it with paging disabled and in a session with no controlling terminal, so the CLI had no terminal to paint a "click to show more" view on \u2014 whatever swallowed the output sits between devinp and `devin` (a shell function or agent wrapper around it, or a broken install).'
|
|
55228
|
+
);
|
|
55229
|
+
} else {
|
|
55230
|
+
let parseError = "";
|
|
55231
|
+
try {
|
|
55232
|
+
JSON.parse(stripAnsi(longest.raw));
|
|
55233
|
+
} catch (error) {
|
|
55234
|
+
parseError = `${error.message} \u2014 `;
|
|
55235
|
+
}
|
|
55236
|
+
lines.push(
|
|
55237
|
+
`Unable to get model prices: \`devin models list --format json\` returned invalid JSON (${parseError}${bytes} bytes, output ends with ${JSON.stringify(tail)}). The output may have been truncated mid-stream.`
|
|
55238
|
+
);
|
|
55239
|
+
}
|
|
55240
|
+
lines.push(`Tried: ${attempts.map((a2) => describeAttempt(a2, timeoutMs)).join("; ")}.`);
|
|
55241
|
+
lines.push("Capture the JSON yourself and hand it to devinp \u2014 piping or redirecting makes `devin` write the whole document in one go:");
|
|
55242
|
+
lines.push(" devin models list --format json > models.json && devinp --source models.json");
|
|
55243
|
+
lines.push(" devin models list --format json | devinp --source=-");
|
|
55244
|
+
lines.push("Check `command -v devin` \u2014 if that is not the real CLI, devinp is asking the wrong program. If a plain redirect still prints a paged view, update the CLI (`devin update`).");
|
|
55245
|
+
return lines.join("\n");
|
|
55246
|
+
}
|
|
55247
|
+
async function readStdin() {
|
|
55248
|
+
const chunks = [];
|
|
55249
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
55250
|
+
return Buffer.concat(chunks.map((c) => typeof c === "string" ? Buffer.from(c) : c)).toString("utf8");
|
|
55251
|
+
}
|
|
55252
|
+
async function loadModelData({
|
|
55253
|
+
source,
|
|
55254
|
+
command = "devin",
|
|
55255
|
+
timeoutMs = DEVIN_TIMEOUT_MS,
|
|
55256
|
+
readStdinFrom = readStdin
|
|
55257
|
+
} = {}) {
|
|
55140
55258
|
if (source) {
|
|
55141
|
-
|
|
55259
|
+
const label = source === "-" ? "stdin" : `"${source}"`;
|
|
55260
|
+
let raw;
|
|
55142
55261
|
try {
|
|
55143
|
-
|
|
55262
|
+
raw = source === "-" ? await readStdinFrom() : await readFile2(source, "utf8");
|
|
55144
55263
|
} catch (error) {
|
|
55145
|
-
throw new Error(`Unable to get model prices: cannot read source file "${source}" (${error.code ?? error.message})`);
|
|
55264
|
+
throw new Error(`Unable to get model prices: cannot read ${source === "-" ? "stdin" : `source file "${source}"`} (${error.code ?? error.message})`);
|
|
55146
55265
|
}
|
|
55147
55266
|
try {
|
|
55148
|
-
return { data: assertFamilies(JSON.parse(
|
|
55267
|
+
return { data: assertFamilies(JSON.parse(raw), label), retries: 0 };
|
|
55149
55268
|
} catch (error) {
|
|
55150
55269
|
if (error.message.startsWith("Unable to get model prices")) throw error;
|
|
55151
|
-
throw new Error(`Unable to get model prices:
|
|
55270
|
+
throw new Error(`Unable to get model prices: ${label} is not valid JSON (expected \`devin models list --format json\` output)`);
|
|
55152
55271
|
}
|
|
55153
55272
|
}
|
|
55154
|
-
let
|
|
55273
|
+
let outcome;
|
|
55155
55274
|
try {
|
|
55156
|
-
|
|
55275
|
+
outcome = await runDevinJson({ command, timeoutMs });
|
|
55157
55276
|
} catch (error) {
|
|
55158
55277
|
if (error.code === "ENOENT") {
|
|
55159
|
-
throw new Error("Unable to get model prices: `devin` CLI not found in PATH. Install it or pass --source <file
|
|
55278
|
+
throw new Error("Unable to get model prices: `devin` CLI not found in PATH. Install it, or pass --source <file> / --source=- to supply the JSON yourself.");
|
|
55160
55279
|
}
|
|
55161
55280
|
throw new Error(`Unable to get model prices: ${error.message}`);
|
|
55162
55281
|
}
|
|
55163
|
-
|
|
55164
|
-
return { data: assertFamilies(
|
|
55165
|
-
}
|
|
55166
|
-
|
|
55167
|
-
|
|
55168
|
-
|
|
55169
|
-
return { data: assertFamilies(recovered, "devin models list (recovered)"), truncated: true };
|
|
55170
|
-
}
|
|
55171
|
-
const tail = raw.trimEnd().slice(-200);
|
|
55172
|
-
throw new Error(
|
|
55173
|
-
`Unable to get model prices: \`devin models list --format json\` returned invalid JSON (${error.message} \u2014 ${raw.length} bytes, output ends with ${JSON.stringify(tail)}). The output may have been truncated mid-stream: re-run, or save the output with \`devin models list --format json > models.json\` and pass \`--source models.json\`.`
|
|
55174
|
-
);
|
|
55282
|
+
if (outcome.parsed != null) {
|
|
55283
|
+
return { data: assertFamilies(outcome.parsed, "devin models list"), retries: outcome.attempts.length - 1 };
|
|
55284
|
+
}
|
|
55285
|
+
const recovered = recoverTruncatedJson(outcome.raw);
|
|
55286
|
+
if (recovered != null) {
|
|
55287
|
+
return { data: assertFamilies(recovered, "devin models list (recovered)"), truncated: true, retries: outcome.attempts.length - 1 };
|
|
55175
55288
|
}
|
|
55289
|
+
throw new Error(describeFailure(outcome.attempts, outcome.timeoutMs));
|
|
55176
55290
|
}
|
|
55177
55291
|
|
|
55178
55292
|
// src/command.js
|
|
@@ -55189,7 +55303,8 @@ var Devinp = class _Devinp extends import_core.Command {
|
|
|
55189
55303
|
"<%= config.bin %> -o=input",
|
|
55190
55304
|
"<%= config.bin %> -o=bench -g=family",
|
|
55191
55305
|
"<%= config.bin %> --web --port 8080",
|
|
55192
|
-
"<%= config.bin %> --source sample/models.json"
|
|
55306
|
+
"<%= config.bin %> --source sample/models.json",
|
|
55307
|
+
"devin models list --format json | <%= config.bin %> --source=-"
|
|
55193
55308
|
];
|
|
55194
55309
|
static flags = {
|
|
55195
55310
|
order: import_core.Flags.string({
|
|
@@ -55214,7 +55329,7 @@ var Devinp = class _Devinp extends import_core.Command {
|
|
|
55214
55329
|
dependsOn: ["web"]
|
|
55215
55330
|
}),
|
|
55216
55331
|
source: import_core.Flags.string({
|
|
55217
|
-
summary: "load prices from a saved `devin models list --format json` file"
|
|
55332
|
+
summary: "load prices from a saved `devin models list --format json` file, or from stdin with --source=- (use it when the CLI pages its output instead of writing it to the pipe)"
|
|
55218
55333
|
}),
|
|
55219
55334
|
json: import_core.Flags.boolean({
|
|
55220
55335
|
summary: "print normalized rows as JSON instead of a table",
|
|
@@ -55229,6 +55344,9 @@ var Devinp = class _Devinp extends import_core.Command {
|
|
|
55229
55344
|
if (loaded.truncated) {
|
|
55230
55345
|
this.warn("devin output was truncated mid-stream \u2014 showing partial data, re-run for the full list");
|
|
55231
55346
|
}
|
|
55347
|
+
if (loaded.retries > 0) {
|
|
55348
|
+
this.warn(`devin needed ${loaded.retries + 1} attempts to hand over the model list \u2014 something on this machine (pager, paged "show more" view, wrapper) interferes with \`devin models list\``);
|
|
55349
|
+
}
|
|
55232
55350
|
data = loaded.data;
|
|
55233
55351
|
} catch (error) {
|
|
55234
55352
|
this.error(error instanceof Error ? error.message : String(error), { exit: 1 });
|