@lucasschirm/devinp 0.1.11 → 0.1.14
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 +47 -13
- package/bin/command.js +61 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,14 +31,32 @@ when a script calls it: the model list comes back paged ("click to show more")
|
|
|
31
31
|
and the JSON never reaches stdout, so `devinp` used to die with *"returned
|
|
32
32
|
invalid JSON (… 0 bytes)"*. It now works harder to get the whole document:
|
|
33
33
|
|
|
34
|
-
-
|
|
35
|
-
`devin`
|
|
36
|
-
real
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
- **A pipe first, so `TMPDIR` cannot break anything.** The happy path captures
|
|
35
|
+
`devin`'s stdout over a pipe — no scratch file, no permissions, no sandbox.
|
|
36
|
+
Measured against the real CLI, the piped document is byte-identical to the
|
|
37
|
+
file capture (471 855 bytes, 46 families), so the old "pipes truncate" worry
|
|
38
|
+
that put a temp file first was misplaced; an unwritable or missing `TMPDIR`
|
|
39
|
+
used to cost two extra `devin` runs, and a missing one even reported *"`devin`
|
|
40
|
+
CLI not found in PATH"*.
|
|
41
|
+
- **Retries that take the terminal away from `devin`.** If an attempt yields no
|
|
42
|
+
usable JSON, `devinp` runs `devin` again, each time stripping more of the
|
|
43
|
+
interactive terminal off it:
|
|
44
|
+
|
|
45
|
+
| # | stdout goes to | environment | controlling terminal |
|
|
46
|
+
| - | ------------ | -------------------------------------------------------- | --------------------- |
|
|
47
|
+
| 1 | pipe | yours, untouched | yes — the healthy path, one `devin` call |
|
|
48
|
+
| 2 | pipe | `PAGER=cat`, `GIT_PAGER=cat`, `TERM=dumb`, `NO_COLOR=1`, `CI=1` | **none** |
|
|
49
|
+
| 3 | scratch file | same | **none** |
|
|
50
|
+
|
|
51
|
+
Spawned in its own session, `devin` cannot open `/dev/tty`: a pager or a TUI
|
|
52
|
+
that paints a "click to show more" view onto the terminal has nowhere to
|
|
53
|
+
draw, so the document has to come out of stdout. Nothing lands on the screen
|
|
54
|
+
you are typing in, and Ctrl-C there no longer kills the lookup. The first
|
|
55
|
+
attempt that parses wins, so the healthy path is unchanged.
|
|
56
|
+
- **The scratch file (attempt 3) walks candidate directories** — `DEVINP_TMP`,
|
|
57
|
+
`os.tmpdir()`, `~/.devinp`, then the current directory — and uses the first
|
|
58
|
+
one it can actually write, removing the file afterwards. Set `DEVINP_TMP` to
|
|
59
|
+
pin it somewhere you control.
|
|
42
60
|
- Noise around the document is tolerated: ANSI colours, an update banner in
|
|
43
61
|
front, or the `pass a slug to --model` hint behind are stripped before the
|
|
44
62
|
JSON is parsed.
|
|
@@ -46,9 +64,11 @@ invalid JSON (… 0 bytes)"*. It now works harder to get the whole document:
|
|
|
46
64
|
prints a warning, and renders the partial table.
|
|
47
65
|
- A real CLI failure (not signed in, network down) is reported as-is instead of
|
|
48
66
|
being retried.
|
|
49
|
-
- If nothing worked, the error lists
|
|
50
|
-
timeout — and how to feed the data in yourself. That always works,
|
|
51
|
-
redirect or a pipe makes `devin` write the whole document in one go:
|
|
67
|
+
- If nothing worked, the error lists all three attempts — bytes, exit code,
|
|
68
|
+
stderr, timeout — and how to feed the data in yourself. That always works,
|
|
69
|
+
because a redirect or a pipe makes `devin` write the whole document in one go:
|
|
70
|
+
(and if it does not, `command -v devin` tells you whether devinp is even
|
|
71
|
+
talking to the CLI and not to a shell function or agent wrapper around it).
|
|
52
72
|
|
|
53
73
|
```sh
|
|
54
74
|
devin models list --format json > models.json && devinp --source models.json
|
|
@@ -125,11 +145,25 @@ so `npm publish`, git-URL installs, and `npm link` always ship a fresh bundle.
|
|
|
125
145
|
|
|
126
146
|
## Publishing
|
|
127
147
|
|
|
128
|
-
`.github/workflows/publish.yml`
|
|
129
|
-
[npm trusted publishing](https://docs.npmjs.
|
|
148
|
+
`.github/workflows/publish.yml` publishes a patch release on every push to main
|
|
149
|
+
with [npm trusted publishing](https://docs.npmjs.org/trusted-publishers) (OIDC —
|
|
130
150
|
no `NPM_TOKEN`). Requires the trusted publisher to be configured on npmjs.com:
|
|
131
151
|
repository `lucasschirm/devinp`, workflow `publish.yml`.
|
|
132
152
|
|
|
153
|
+
It **never commits to main** — the branch ruleset requires every change to
|
|
154
|
+
arrive through a pull request, and a rejected release commit used to strand the
|
|
155
|
+
publish step that ran after it (GH013, then `npm publish` skipped). The released
|
|
156
|
+
version therefore lives in the npm registry and in the git tag, not in main's
|
|
157
|
+
`package.json`:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
git ls-remote --tags origin # released versions
|
|
161
|
+
npm view @lucasschirm/devinp version # latest published
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The workflow takes the first patch number that is free in both places, so a tag
|
|
165
|
+
left behind by an older run cannot wedge it.
|
|
166
|
+
|
|
133
167
|
## License
|
|
134
168
|
|
|
135
169
|
MIT
|
package/bin/command.js
CHANGED
|
@@ -26885,7 +26885,7 @@ var require_sonic_boom = __commonJS({
|
|
|
26885
26885
|
if (!(this instanceof SonicBoom)) {
|
|
26886
26886
|
return new SonicBoom(opts);
|
|
26887
26887
|
}
|
|
26888
|
-
let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir, retryEAGAIN, fsync, contentMode, mode } = opts || {};
|
|
26888
|
+
let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir: mkdir2, retryEAGAIN, fsync, contentMode, mode } = opts || {};
|
|
26889
26889
|
fd = fd || dest;
|
|
26890
26890
|
this._len = 0;
|
|
26891
26891
|
this.fd = -1;
|
|
@@ -26910,7 +26910,7 @@ var require_sonic_boom = __commonJS({
|
|
|
26910
26910
|
this.append = append || false;
|
|
26911
26911
|
this.mode = mode;
|
|
26912
26912
|
this.retryEAGAIN = retryEAGAIN || (() => true);
|
|
26913
|
-
this.mkdir =
|
|
26913
|
+
this.mkdir = mkdir2 || false;
|
|
26914
26914
|
let fsWriteSync;
|
|
26915
26915
|
let fsWrite;
|
|
26916
26916
|
if (contentMode === kContentModeBuffer) {
|
|
@@ -55024,8 +55024,8 @@ async function startServer({ rows, benchMeta, port, log }) {
|
|
|
55024
55024
|
|
|
55025
55025
|
// src/lib/source.js
|
|
55026
55026
|
import { spawn } from "node:child_process";
|
|
55027
|
-
import { open, readFile as readFile2, unlink } from "node:fs/promises";
|
|
55028
|
-
import { tmpdir } from "node:os";
|
|
55027
|
+
import { mkdir, open, readFile as readFile2, unlink } from "node:fs/promises";
|
|
55028
|
+
import { homedir, tmpdir } from "node:os";
|
|
55029
55029
|
import { join as join2 } from "node:path";
|
|
55030
55030
|
|
|
55031
55031
|
// src/lib/repair-json.js
|
|
@@ -55095,10 +55095,11 @@ var NON_INTERACTIVE_ENV = {
|
|
|
55095
55095
|
NO_COLOR: "1",
|
|
55096
55096
|
CI: "1"
|
|
55097
55097
|
};
|
|
55098
|
+
var DETACHED_SPAWN = { detached: true, windowsHide: true };
|
|
55098
55099
|
var STRATEGIES = [
|
|
55099
|
-
{ label: "
|
|
55100
|
-
{ label: "
|
|
55101
|
-
{ label: "
|
|
55100
|
+
{ label: "pipe", file: false, env: null },
|
|
55101
|
+
{ label: "pipe, paging disabled, no controlling terminal", file: false, env: NON_INTERACTIVE_ENV, detached: true },
|
|
55102
|
+
{ label: "temp file, paging disabled, no controlling terminal", file: true, env: NON_INTERACTIVE_ENV, detached: true }
|
|
55102
55103
|
];
|
|
55103
55104
|
var ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][\s\S]*?(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g;
|
|
55104
55105
|
function stripAnsi(text) {
|
|
@@ -55120,11 +55121,12 @@ function parseJsonLoosely(text) {
|
|
|
55120
55121
|
}
|
|
55121
55122
|
return null;
|
|
55122
55123
|
}
|
|
55123
|
-
function spawnDevin({ command, env, stdout, timeoutMs }) {
|
|
55124
|
+
function spawnDevin({ command, env, stdout, timeoutMs, detached = false }) {
|
|
55124
55125
|
return new Promise((resolve, reject) => {
|
|
55125
55126
|
const child = spawn(command, ["models", "list", "--format", "json"], {
|
|
55126
55127
|
stdio: ["ignore", stdout, "pipe"],
|
|
55127
|
-
...env ? { env: { ...process.env, ...env } } : {}
|
|
55128
|
+
...env ? { env: { ...process.env, ...env } } : {},
|
|
55129
|
+
...detached ? DETACHED_SPAWN : {}
|
|
55128
55130
|
});
|
|
55129
55131
|
let stdoutText = "";
|
|
55130
55132
|
let stderr = "";
|
|
@@ -55140,6 +55142,7 @@ function spawnDevin({ command, env, stdout, timeoutMs }) {
|
|
|
55140
55142
|
stderr += chunk;
|
|
55141
55143
|
});
|
|
55142
55144
|
child.on("error", (error) => {
|
|
55145
|
+
error.devinSpawn = true;
|
|
55143
55146
|
clearTimeout(timer);
|
|
55144
55147
|
reject(error);
|
|
55145
55148
|
});
|
|
@@ -55149,11 +55152,41 @@ function spawnDevin({ command, env, stdout, timeoutMs }) {
|
|
|
55149
55152
|
});
|
|
55150
55153
|
});
|
|
55151
55154
|
}
|
|
55152
|
-
async function
|
|
55153
|
-
const
|
|
55154
|
-
|
|
55155
|
+
async function writableScratchDir() {
|
|
55156
|
+
const candidates = [];
|
|
55157
|
+
if (process.env.DEVINP_TMP) candidates.push(process.env.DEVINP_TMP);
|
|
55158
|
+
candidates.push(tmpdir(), join2(homedir(), ".devinp"), process.cwd());
|
|
55159
|
+
for (const dir of candidates) {
|
|
55160
|
+
try {
|
|
55161
|
+
await mkdir(dir, { recursive: true });
|
|
55162
|
+
const probe = join2(dir, `.devinp-probe-${process.pid}`);
|
|
55163
|
+
const handle = await open(probe, "w");
|
|
55164
|
+
await handle.close();
|
|
55165
|
+
await unlink(probe);
|
|
55166
|
+
return dir;
|
|
55167
|
+
} catch {
|
|
55168
|
+
}
|
|
55169
|
+
}
|
|
55170
|
+
return null;
|
|
55171
|
+
}
|
|
55172
|
+
async function captureToFile({ command, env, timeoutMs, detached }) {
|
|
55173
|
+
const dir = await writableScratchDir();
|
|
55174
|
+
if (!dir) {
|
|
55175
|
+
const error = new Error(`cannot write a scratch file \u2014 DEVINP_TMP, os.tmpdir() ("${tmpdir()}"), ~/.devinp and "${process.cwd()}" all refused`);
|
|
55176
|
+
error.scratchPath = dir;
|
|
55177
|
+
throw error;
|
|
55178
|
+
}
|
|
55179
|
+
const file = join2(dir, `devinp-models-${process.pid}-${Date.now()}.json`);
|
|
55180
|
+
let handle;
|
|
55155
55181
|
try {
|
|
55156
|
-
|
|
55182
|
+
handle = await open(file, "w");
|
|
55183
|
+
} catch (error) {
|
|
55184
|
+
error.message = `cannot write scratch file "${file}" (${error.code ?? error.message})`;
|
|
55185
|
+
error.scratchPath = file;
|
|
55186
|
+
throw error;
|
|
55187
|
+
}
|
|
55188
|
+
try {
|
|
55189
|
+
const result = await spawnDevin({ command, env, stdout: handle.fd, timeoutMs, detached });
|
|
55157
55190
|
result.stdout = await readFile2(file, "utf8");
|
|
55158
55191
|
return result;
|
|
55159
55192
|
} finally {
|
|
@@ -55167,9 +55200,9 @@ async function runDevinJson({ command = "devin", timeoutMs = DEVIN_TIMEOUT_MS }
|
|
|
55167
55200
|
for (const [index, strategy] of STRATEGIES.entries()) {
|
|
55168
55201
|
let result;
|
|
55169
55202
|
try {
|
|
55170
|
-
result = strategy.file ? await captureToFile({ command, env: strategy.env, timeoutMs }) : await spawnDevin({ command, env: strategy.env, stdout: "pipe", timeoutMs });
|
|
55203
|
+
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 });
|
|
55171
55204
|
} catch (error) {
|
|
55172
|
-
if (error.code === "ENOENT") throw error;
|
|
55205
|
+
if (error.code === "ENOENT" && error.devinSpawn) throw error;
|
|
55173
55206
|
attempts.push({ label: strategy.label, bytes: 0, code: null, stderr: "", timedOut: false, note: error.message });
|
|
55174
55207
|
continue;
|
|
55175
55208
|
}
|
|
@@ -55183,7 +55216,7 @@ async function runDevinJson({ command = "devin", timeoutMs = DEVIN_TIMEOUT_MS }
|
|
|
55183
55216
|
timedOut: result.timedOut,
|
|
55184
55217
|
raw
|
|
55185
55218
|
});
|
|
55186
|
-
if (parsed != null) return { raw, parsed, attempts, timeoutMs };
|
|
55219
|
+
if (parsed != null) return { raw, parsed, attempts, timeoutMs, via: strategy.label };
|
|
55187
55220
|
if (index === 0 && result.code !== 0 && !result.timedOut && result.stderr.trim()) {
|
|
55188
55221
|
throw new Error(`\`devin models list\` failed \u2014 ${result.stderr.trim()}`);
|
|
55189
55222
|
}
|
|
@@ -55192,8 +55225,8 @@ async function runDevinJson({ command = "devin", timeoutMs = DEVIN_TIMEOUT_MS }
|
|
|
55192
55225
|
if (explained && attempts.every((a2) => a2.timedOut || a2.code !== 0)) {
|
|
55193
55226
|
throw new Error(`\`devin models list\` failed \u2014 ${explained.stderr}`);
|
|
55194
55227
|
}
|
|
55195
|
-
const longest = attempts.reduce((acc, a2) => (a2.raw?.length ?? 0)
|
|
55196
|
-
return { raw: longest || "", parsed: null, attempts, timeoutMs };
|
|
55228
|
+
const longest = attempts.reduce((acc, a2) => (a2.raw?.length ?? 0) >= acc.length ? a2.raw : acc, "");
|
|
55229
|
+
return { raw: longest || "", parsed: null, attempts, timeoutMs, via: null };
|
|
55197
55230
|
}
|
|
55198
55231
|
function assertFamilies(data, sourceName) {
|
|
55199
55232
|
if (data == null || typeof data !== "object" || !Array.isArray(data.families)) {
|
|
@@ -55222,7 +55255,7 @@ function describeFailure(attempts, timeoutMs) {
|
|
|
55222
55255
|
const lines = [];
|
|
55223
55256
|
if (bytes === 0) {
|
|
55224
55257
|
lines.push(
|
|
55225
|
-
'Unable to get model prices: `devin models list --format json` exited without writing the JSON to stdout (0 bytes).
|
|
55258
|
+
'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).'
|
|
55226
55259
|
);
|
|
55227
55260
|
} else {
|
|
55228
55261
|
let parseError = "";
|
|
@@ -55236,10 +55269,13 @@ function describeFailure(attempts, timeoutMs) {
|
|
|
55236
55269
|
);
|
|
55237
55270
|
}
|
|
55238
55271
|
lines.push(`Tried: ${attempts.map((a2) => describeAttempt(a2, timeoutMs)).join("; ")}.`);
|
|
55272
|
+
if (attempts.some((a2) => a2.note?.includes("scratch"))) {
|
|
55273
|
+
lines.push("devinp could not write its scratch file \u2014 point it somewhere writable with `DEVINP_TMP=/tmp devinp`.");
|
|
55274
|
+
}
|
|
55239
55275
|
lines.push("Capture the JSON yourself and hand it to devinp \u2014 piping or redirecting makes `devin` write the whole document in one go:");
|
|
55240
55276
|
lines.push(" devin models list --format json > models.json && devinp --source models.json");
|
|
55241
55277
|
lines.push(" devin models list --format json | devinp --source=-");
|
|
55242
|
-
lines.push("
|
|
55278
|
+
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`).");
|
|
55243
55279
|
return lines.join("\n");
|
|
55244
55280
|
}
|
|
55245
55281
|
async function readStdin() {
|
|
@@ -55262,7 +55298,7 @@ async function loadModelData({
|
|
|
55262
55298
|
throw new Error(`Unable to get model prices: cannot read ${source === "-" ? "stdin" : `source file "${source}"`} (${error.code ?? error.message})`);
|
|
55263
55299
|
}
|
|
55264
55300
|
try {
|
|
55265
|
-
return { data: assertFamilies(JSON.parse(raw), label), retries: 0 };
|
|
55301
|
+
return { data: assertFamilies(JSON.parse(raw), label), retries: 0, via: source === "-" ? "stdin" : "file" };
|
|
55266
55302
|
} catch (error) {
|
|
55267
55303
|
if (error.message.startsWith("Unable to get model prices")) throw error;
|
|
55268
55304
|
throw new Error(`Unable to get model prices: ${label} is not valid JSON (expected \`devin models list --format json\` output)`);
|
|
@@ -55272,17 +55308,17 @@ async function loadModelData({
|
|
|
55272
55308
|
try {
|
|
55273
55309
|
outcome = await runDevinJson({ command, timeoutMs });
|
|
55274
55310
|
} catch (error) {
|
|
55275
|
-
if (error.code === "ENOENT") {
|
|
55311
|
+
if (error.code === "ENOENT" && error.devinSpawn) {
|
|
55276
55312
|
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.");
|
|
55277
55313
|
}
|
|
55278
55314
|
throw new Error(`Unable to get model prices: ${error.message}`);
|
|
55279
55315
|
}
|
|
55280
55316
|
if (outcome.parsed != null) {
|
|
55281
|
-
return { data: assertFamilies(outcome.parsed, "devin models list"), retries: outcome.attempts.length - 1 };
|
|
55317
|
+
return { data: assertFamilies(outcome.parsed, "devin models list"), retries: outcome.attempts.length - 1, via: outcome.via };
|
|
55282
55318
|
}
|
|
55283
55319
|
const recovered = recoverTruncatedJson(outcome.raw);
|
|
55284
55320
|
if (recovered != null) {
|
|
55285
|
-
return { data: assertFamilies(recovered, "devin models list (recovered)"), truncated: true, retries: outcome.attempts.length - 1 };
|
|
55321
|
+
return { data: assertFamilies(recovered, "devin models list (recovered)"), truncated: true, retries: outcome.attempts.length - 1, via: outcome.via ?? "repaired" };
|
|
55286
55322
|
}
|
|
55287
55323
|
throw new Error(describeFailure(outcome.attempts, outcome.timeoutMs));
|
|
55288
55324
|
}
|
|
@@ -55343,7 +55379,7 @@ var Devinp = class _Devinp extends import_core.Command {
|
|
|
55343
55379
|
this.warn("devin output was truncated mid-stream \u2014 showing partial data, re-run for the full list");
|
|
55344
55380
|
}
|
|
55345
55381
|
if (loaded.retries > 0) {
|
|
55346
|
-
this.warn(`devin needed ${loaded.retries + 1} attempts to hand over the model list \u2014
|
|
55382
|
+
this.warn(`devin needed ${loaded.retries + 1} attempts to hand over the model list \u2014 the data arrived via ${loaded.via ?? "a fallback"} (something on this machine interferes with \`devin models list\`; DEVINP_TMP sets the scratch directory)`);
|
|
55347
55383
|
}
|
|
55348
55384
|
data = loaded.data;
|
|
55349
55385
|
} catch (error) {
|