@drael/code 0.2.0 → 0.4.0
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 +21 -57
- package/package.json +1 -1
- package/src/code.js +69 -16
package/README.md
CHANGED
|
@@ -1,84 +1,48 @@
|
|
|
1
1
|
# @drael/code
|
|
2
2
|
|
|
3
|
-
Points a coding client at [Drael](https://drael.sh).
|
|
4
|
-
writes.
|
|
3
|
+
Points a coding client at [Drael](https://drael.sh).
|
|
5
4
|
|
|
6
5
|
```sh
|
|
7
6
|
npx @drael/code
|
|
8
7
|
```
|
|
9
8
|
|
|
10
|
-
It asks for your key and which client,
|
|
9
|
+
It asks for your key and which client, writes that client's own configuration file, and
|
|
10
|
+
nothing else. Whatever it touches it copies first.
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
npx @drael/code --
|
|
13
|
+
npx @drael/code --list what it knows how to configure, and where
|
|
14
|
+
npx @drael/code --dry-run exactly what it would write, changing nothing
|
|
15
|
+
npx @drael/code --uninstall every file it changed, put back
|
|
16
|
+
npx @drael/code --help the flags
|
|
14
17
|
```
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
| Client | File |
|
|
19
|
-
| ---------- | ----------------------------- |
|
|
20
|
-
| `opencode` | `~/.config/opencode/opencode.json` |
|
|
21
|
-
| `kilo` | `~/.config/kilo/kilo.jsonc` |
|
|
22
|
-
| `env` | `~/.config/drael/env.sh`, for anything reading `OPENAI_BASE_URL` |
|
|
23
|
-
|
|
24
|
-
All three are written `0600`, because a file holding an API key is not a file other
|
|
25
|
-
accounts on the machine should read.
|
|
26
|
-
|
|
27
|
-
The `kilo` file is **merged** rather than replaced: it is the whole extension's
|
|
28
|
-
configuration and not ours, so the other providers and everything else in it are kept.
|
|
29
|
-
|
|
30
|
-
## Undoing it
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
npx @drael/code --uninstall
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Every file is copied before it is written, under `~/.local/state/drael/installer`.
|
|
37
|
-
Uninstall puts the originals back byte for byte and removes the files that did not exist
|
|
38
|
-
before. Installing twice keeps the **first** copy, so what an uninstall restores is the
|
|
39
|
-
state before this ever touched the file.
|
|
40
|
-
|
|
41
|
-
`--dry-run` prints exactly what would be written and changes nothing, including the copy.
|
|
42
|
-
|
|
43
|
-
## The flags
|
|
44
|
-
|
|
45
|
-
| Flag | What it does |
|
|
46
|
-
| ------------- | ---------------------------------------------------------------- |
|
|
47
|
-
| `--key` | The API key. Falls back to `DRAEL_KEY`, then to a prompt. |
|
|
48
|
-
| `--client` | `opencode`, `kilo` or `env`. Without it you are asked. |
|
|
49
|
-
| `--host` | For a local build or your own box. Defaults to `https://drael.sh`. |
|
|
50
|
-
| `--list` | Prints what it knows how to configure, and exits. |
|
|
51
|
-
| `--dry-run` | Prints what would be written, and changes nothing. |
|
|
52
|
-
| `--uninstall` | Restores every file it changed, and exits. |
|
|
19
|
+
Those four answer for themselves rather than being copied out here, so this page cannot
|
|
20
|
+
go stale about which clients are supported, where their files live, or what the flags are.
|
|
53
21
|
|
|
54
22
|
## Why npx rather than `curl | sh`
|
|
55
23
|
|
|
56
24
|
Piping a URL into a shell is exactly the pattern this product's own users are right to
|
|
57
|
-
refuse. npx is already on the machine of anybody running an editor extension, and it
|
|
58
|
-
|
|
25
|
+
refuse. npx is already on the machine of anybody running an editor extension, and it makes
|
|
26
|
+
Windows work without a second implementation to keep in step.
|
|
59
27
|
|
|
60
|
-
It
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
That call is not a check. The last thing it prints is the `curl` that verifies the host
|
|
65
|
-
and the key, for you to run yourself.
|
|
66
|
-
|
|
67
|
-
The source is one file with one dependency, and it is short enough to read in a sitting.
|
|
68
|
-
The people most right to distrust an opaque installer are this product's own users, so it
|
|
69
|
-
earns the trust by being readable rather than by asking.
|
|
28
|
+
It asks for no privilege, and it writes nothing outside the configuration file of the
|
|
29
|
+
client you chose. The source is one file, short enough to read in a sitting: the people
|
|
30
|
+
most right to distrust an opaque installer are this product's own users, so it earns the
|
|
31
|
+
trust by being readable rather than by asking.
|
|
70
32
|
|
|
71
33
|
## Doing it by hand
|
|
72
34
|
|
|
73
|
-
There is nothing privileged in what it writes.
|
|
74
|
-
|
|
35
|
+
There is nothing privileged in what it writes. The API is OpenAI-compatible, so any client
|
|
36
|
+
takes a base URL and a key directly:
|
|
75
37
|
|
|
76
38
|
```sh
|
|
77
39
|
export OPENAI_BASE_URL="https://drael.sh/v1"
|
|
78
40
|
export OPENAI_API_KEY="dk-YOUR-KEY"
|
|
79
41
|
```
|
|
80
42
|
|
|
81
|
-
|
|
82
|
-
|
|
43
|
+
## Everything else
|
|
44
|
+
|
|
45
|
+
[drael.sh/docs/install](https://drael.sh/docs/install) is the documentation, and it is the
|
|
46
|
+
copy that is kept current.
|
|
83
47
|
|
|
84
48
|
MIT
|
package/package.json
CHANGED
package/src/code.js
CHANGED
|
@@ -41,12 +41,15 @@ import { isCancel, PasswordPrompt, SelectPrompt } from '@clack/core'
|
|
|
41
41
|
|
|
42
42
|
/* ───────────────────────────────────────────────────────────── where things live ── */
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
// Home-relative on every platform, Windows included. The clients here document
|
|
45
|
+
// `~/.config/<them>` literally and read nowhere else, so the earlier `%APPDATA%\\Config`
|
|
46
|
+
// branch wrote a valid file into a directory nothing opens: the worst failure this tool
|
|
47
|
+
// has, because it reports success.
|
|
48
|
+
const CONFIG_HOME = process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config')
|
|
49
|
+
|
|
50
|
+
// The backups do not follow, and that is deliberate: this directory is ours rather than a
|
|
51
|
+
// client's, so it was never in the wrong place, and moving it would strand the records
|
|
52
|
+
// written by an earlier version. An uninstall that cannot find them is not an uninstall.
|
|
50
53
|
const STATE_HOME =
|
|
51
54
|
process.env.XDG_STATE_HOME ??
|
|
52
55
|
(platform() === 'win32'
|
|
@@ -55,8 +58,8 @@ const STATE_HOME =
|
|
|
55
58
|
|
|
56
59
|
const BACKUPS = join(STATE_HOME, 'drael', 'installer')
|
|
57
60
|
|
|
58
|
-
// Kilo documents this literal path
|
|
59
|
-
//
|
|
61
|
+
// Kilo documents this literal path, so unlike the others it is not moved by
|
|
62
|
+
// XDG_CONFIG_HOME either.
|
|
60
63
|
const KILO_CONFIG = join(homedir(), '.config', 'kilo', 'kilo.jsonc')
|
|
61
64
|
|
|
62
65
|
// There is one origin (ADR 0017), and this package is published by the people who run
|
|
@@ -279,17 +282,24 @@ const askClient = () =>
|
|
|
279
282
|
* product itself. The one call this makes, and the only one: it is to the host being
|
|
280
283
|
* configured, with the key being configured, and a host that does not answer is not a
|
|
281
284
|
* reason to refuse to write anything, so the published id stands in.
|
|
285
|
+
*
|
|
286
|
+
* It is also the only check the reader needs, which is why the frame reports what came
|
|
287
|
+
* back instead of handing out a curl line: that line carried the key in clear text, on
|
|
288
|
+
* screen and into the scrollback, one row under the prompt that had just masked it.
|
|
282
289
|
*/
|
|
283
|
-
async function
|
|
290
|
+
async function askHost(host, key) {
|
|
284
291
|
try {
|
|
285
292
|
const response = await fetch(`${host}/v1/models`, {
|
|
286
293
|
headers: { Authorization: `Bearer ${key}` },
|
|
287
294
|
signal: AbortSignal.timeout(4000),
|
|
288
295
|
})
|
|
296
|
+
if (response.status === 401 || response.status === 403) {
|
|
297
|
+
return { refused: true }
|
|
298
|
+
}
|
|
289
299
|
const served = (await response.json())?.data?.[0]?.id
|
|
290
|
-
return typeof served === 'string' && served ? served :
|
|
300
|
+
return { model: typeof served === 'string' && served ? served : null }
|
|
291
301
|
} catch {
|
|
292
|
-
return
|
|
302
|
+
return {}
|
|
293
303
|
}
|
|
294
304
|
}
|
|
295
305
|
|
|
@@ -327,8 +337,9 @@ async function install(args) {
|
|
|
327
337
|
fail(`unknown client: ${name} (try --list)`)
|
|
328
338
|
}
|
|
329
339
|
|
|
340
|
+
const answer = await askHost(host, key)
|
|
330
341
|
const file = client.file()
|
|
331
|
-
const contents = client.contents(host, key,
|
|
342
|
+
const contents = client.contents(host, key, answer.model ?? MODEL)
|
|
332
343
|
|
|
333
344
|
// An answered prompt leaves its frame on screen and needs a gap after it. With every
|
|
334
345
|
// answer given as a flag there was no frame, and the masthead's own gap is already it.
|
|
@@ -367,11 +378,25 @@ async function install(args) {
|
|
|
367
378
|
console.log()
|
|
368
379
|
console.log(rule())
|
|
369
380
|
console.log()
|
|
370
|
-
console.log(row('
|
|
381
|
+
console.log(row('the host', dim(`${host}/v1, ${said(answer)}`)))
|
|
382
|
+
if (answer.refused) {
|
|
383
|
+
console.log(under(dim(`create one at ${host}/developers, then run this again`)))
|
|
384
|
+
}
|
|
371
385
|
console.log(row('undo it', dim('npx @drael/code --uninstall')))
|
|
372
386
|
console.log()
|
|
373
387
|
}
|
|
374
388
|
|
|
389
|
+
/** What the one call came back with, in the frame's own voice. */
|
|
390
|
+
function said(answer) {
|
|
391
|
+
if (answer.refused) {
|
|
392
|
+
return 'which refused that key'
|
|
393
|
+
}
|
|
394
|
+
if (answer.model) {
|
|
395
|
+
return `serving ${answer.model}`
|
|
396
|
+
}
|
|
397
|
+
return `which did not answer, so ${MODEL} was written`
|
|
398
|
+
}
|
|
399
|
+
|
|
375
400
|
/**
|
|
376
401
|
* Every write is preceded by a copy. An uninstall that cannot put the file back is not
|
|
377
402
|
* an uninstall, it is a deletion with better manners. It reports what it found rather
|
|
@@ -443,6 +468,26 @@ function uninstall(args) {
|
|
|
443
468
|
console.log()
|
|
444
469
|
}
|
|
445
470
|
|
|
471
|
+
/**
|
|
472
|
+
* The flags, from the tool rather than from a page about the tool. The README points here
|
|
473
|
+
* instead of repeating any of it, and a test holds this list to the flags the code reads.
|
|
474
|
+
*/
|
|
475
|
+
function help() {
|
|
476
|
+
masthead('into the editor you already use')
|
|
477
|
+
for (const [flag, said] of [
|
|
478
|
+
['--key', 'the API key. Falls back to DRAEL_KEY, then to a prompt'],
|
|
479
|
+
['--client', 'which one to configure. Without it, you are asked'],
|
|
480
|
+
['--host', `for a local build or your own box. Defaults to ${DRAEL}`],
|
|
481
|
+
['--list', 'what it knows how to configure, and where'],
|
|
482
|
+
['--dry-run', 'exactly what it would write, changing nothing'],
|
|
483
|
+
['--uninstall', 'every file it changed, put back'],
|
|
484
|
+
['--help', 'this'],
|
|
485
|
+
]) {
|
|
486
|
+
console.log(row(flag, dim(said)))
|
|
487
|
+
}
|
|
488
|
+
console.log()
|
|
489
|
+
}
|
|
490
|
+
|
|
446
491
|
function list() {
|
|
447
492
|
console.log()
|
|
448
493
|
for (const [name, client] of Object.entries(clients)) {
|
|
@@ -473,11 +518,17 @@ function parse(argv) {
|
|
|
473
518
|
const arg = argv[i]
|
|
474
519
|
if (!arg.startsWith('--')) continue
|
|
475
520
|
const name = arg.slice(2)
|
|
476
|
-
if (['list', 'uninstall', 'dry-run'].includes(name)) {
|
|
521
|
+
if (['list', 'uninstall', 'dry-run', 'help'].includes(name)) {
|
|
477
522
|
parsed[name] = true
|
|
478
523
|
continue
|
|
479
524
|
}
|
|
480
|
-
|
|
525
|
+
// `--client --key dk-…` used to swallow the next flag as the value and then report
|
|
526
|
+
// `unknown client: --key`, which names neither the mistake nor the fix.
|
|
527
|
+
const value = argv[++i]
|
|
528
|
+
if (value === undefined || value.startsWith('--')) {
|
|
529
|
+
fail(`--${name} needs a value`)
|
|
530
|
+
}
|
|
531
|
+
parsed[name] = value
|
|
481
532
|
}
|
|
482
533
|
return parsed
|
|
483
534
|
}
|
|
@@ -488,7 +539,9 @@ process.on('exit', () => process.stdout.write('\u001b[?25h'))
|
|
|
488
539
|
|
|
489
540
|
const args = parse(process.argv.slice(2))
|
|
490
541
|
|
|
491
|
-
if (args.
|
|
542
|
+
if (args.help) {
|
|
543
|
+
help()
|
|
544
|
+
} else if (args.list) {
|
|
492
545
|
list()
|
|
493
546
|
} else if (args.uninstall) {
|
|
494
547
|
uninstall(args)
|