@agentuity/cli 0.0.43 → 0.0.44
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/AGENTS.md +1 -1
- package/README.md +1 -1
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/auth.d.ts +10 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/banner.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cmd/auth/api.d.ts +4 -4
- package/dist/cmd/auth/api.d.ts.map +1 -1
- package/dist/cmd/auth/index.d.ts.map +1 -1
- package/dist/cmd/auth/login.d.ts.map +1 -1
- package/dist/cmd/auth/signup.d.ts.map +1 -1
- package/dist/cmd/auth/ssh/add.d.ts +2 -0
- package/dist/cmd/auth/ssh/add.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/api.d.ts +16 -0
- package/dist/cmd/auth/ssh/api.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/delete.d.ts +2 -0
- package/dist/cmd/auth/ssh/delete.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/index.d.ts +3 -0
- package/dist/cmd/auth/ssh/index.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/list.d.ts +2 -0
- package/dist/cmd/auth/ssh/list.d.ts.map +1 -0
- package/dist/cmd/auth/whoami.d.ts.map +1 -1
- package/dist/cmd/bundle/ast.d.ts +14 -3
- package/dist/cmd/bundle/ast.d.ts.map +1 -1
- package/dist/cmd/bundle/ast.test.d.ts +2 -0
- package/dist/cmd/bundle/ast.test.d.ts.map +1 -0
- package/dist/cmd/bundle/bundler.d.ts +6 -1
- package/dist/cmd/bundle/bundler.d.ts.map +1 -1
- package/dist/cmd/bundle/file.d.ts.map +1 -1
- package/dist/cmd/bundle/fix-duplicate-exports.d.ts +2 -0
- package/dist/cmd/bundle/fix-duplicate-exports.d.ts.map +1 -0
- package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts +2 -0
- package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts.map +1 -0
- package/dist/cmd/bundle/plugin.d.ts +2 -0
- package/dist/cmd/bundle/plugin.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/domain.d.ts +17 -0
- package/dist/cmd/cloud/domain.d.ts.map +1 -0
- package/dist/cmd/cloud/index.d.ts.map +1 -1
- package/dist/cmd/cloud/resource/add.d.ts +2 -0
- package/dist/cmd/cloud/resource/add.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/delete.d.ts +2 -0
- package/dist/cmd/cloud/resource/delete.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/index.d.ts +3 -0
- package/dist/cmd/cloud/resource/index.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/list.d.ts +2 -0
- package/dist/cmd/cloud/resource/list.d.ts.map +1 -0
- package/dist/cmd/cloud/scp/download.d.ts +2 -0
- package/dist/cmd/cloud/scp/download.d.ts.map +1 -0
- package/dist/cmd/cloud/scp/index.d.ts +3 -0
- package/dist/cmd/cloud/scp/index.d.ts.map +1 -0
- package/dist/cmd/cloud/scp/upload.d.ts +2 -0
- package/dist/cmd/cloud/scp/upload.d.ts.map +1 -0
- package/dist/cmd/cloud/ssh.d.ts +2 -0
- package/dist/cmd/cloud/ssh.d.ts.map +1 -0
- package/dist/cmd/dev/api.d.ts +18 -0
- package/dist/cmd/dev/api.d.ts.map +1 -0
- package/dist/cmd/dev/download.d.ts +11 -0
- package/dist/cmd/dev/download.d.ts.map +1 -0
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/templates.d.ts +3 -0
- package/dist/cmd/dev/templates.d.ts.map +1 -0
- package/dist/cmd/env/delete.d.ts.map +1 -1
- package/dist/cmd/env/get.d.ts.map +1 -1
- package/dist/cmd/env/import.d.ts.map +1 -1
- package/dist/cmd/env/list.d.ts.map +1 -1
- package/dist/cmd/env/pull.d.ts.map +1 -1
- package/dist/cmd/env/push.d.ts.map +1 -1
- package/dist/cmd/env/set.d.ts.map +1 -1
- package/dist/cmd/profile/show.d.ts.map +1 -1
- package/dist/cmd/project/create.d.ts.map +1 -1
- package/dist/cmd/project/delete.d.ts.map +1 -1
- package/dist/cmd/project/list.d.ts.map +1 -1
- package/dist/cmd/project/show.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.d.ts +4 -0
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/secret/delete.d.ts.map +1 -1
- package/dist/cmd/secret/get.d.ts.map +1 -1
- package/dist/cmd/secret/import.d.ts.map +1 -1
- package/dist/cmd/secret/list.d.ts.map +1 -1
- package/dist/cmd/secret/pull.d.ts.map +1 -1
- package/dist/cmd/secret/push.d.ts.map +1 -1
- package/dist/cmd/secret/set.d.ts.map +1 -1
- package/dist/config.d.ts +9 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/crypto/box.d.ts +65 -0
- package/dist/crypto/box.d.ts.map +1 -0
- package/dist/crypto/box.test.d.ts +2 -0
- package/dist/crypto/box.test.d.ts.map +1 -0
- package/dist/download.d.ts.map +1 -1
- package/dist/steps.d.ts +4 -1
- package/dist/steps.d.ts.map +1 -1
- package/dist/terminal.d.ts.map +1 -1
- package/dist/tui.d.ts +31 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/types.d.ts +249 -126
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/detectSubagent.d.ts +15 -0
- package/dist/utils/detectSubagent.d.ts.map +1 -0
- package/dist/utils/zip.d.ts +7 -0
- package/dist/utils/zip.d.ts.map +1 -0
- package/package.json +11 -3
- package/src/api-errors.md +2 -2
- package/src/api.ts +12 -7
- package/src/auth.ts +116 -7
- package/src/banner.ts +13 -6
- package/src/cli.ts +695 -63
- package/src/cmd/auth/api.ts +10 -16
- package/src/cmd/auth/index.ts +2 -1
- package/src/cmd/auth/login.ts +24 -8
- package/src/cmd/auth/signup.ts +15 -11
- package/src/cmd/auth/ssh/add.ts +263 -0
- package/src/cmd/auth/ssh/api.ts +94 -0
- package/src/cmd/auth/ssh/delete.ts +102 -0
- package/src/cmd/auth/ssh/index.ts +10 -0
- package/src/cmd/auth/ssh/list.ts +74 -0
- package/src/cmd/auth/whoami.ts +13 -13
- package/src/cmd/bundle/ast.test.ts +565 -0
- package/src/cmd/bundle/ast.ts +457 -44
- package/src/cmd/bundle/bundler.ts +255 -57
- package/src/cmd/bundle/file.ts +6 -12
- package/src/cmd/bundle/fix-duplicate-exports.test.ts +387 -0
- package/src/cmd/bundle/fix-duplicate-exports.ts +204 -0
- package/src/cmd/bundle/index.ts +9 -9
- package/src/cmd/bundle/patch/aisdk.ts +1 -1
- package/src/cmd/bundle/plugin.ts +373 -53
- package/src/cmd/cloud/deploy.ts +300 -93
- package/src/cmd/cloud/domain.ts +92 -0
- package/src/cmd/cloud/index.ts +4 -1
- package/src/cmd/cloud/resource/add.ts +56 -0
- package/src/cmd/cloud/resource/delete.ts +120 -0
- package/src/cmd/cloud/resource/index.ts +11 -0
- package/src/cmd/cloud/resource/list.ts +69 -0
- package/src/cmd/cloud/scp/download.ts +59 -0
- package/src/cmd/cloud/scp/index.ts +9 -0
- package/src/cmd/cloud/scp/upload.ts +62 -0
- package/src/cmd/cloud/ssh.ts +68 -0
- package/src/cmd/dev/api.ts +46 -0
- package/src/cmd/dev/download.ts +111 -0
- package/src/cmd/dev/index.ts +360 -34
- package/src/cmd/dev/templates.ts +84 -0
- package/src/cmd/env/delete.ts +5 -20
- package/src/cmd/env/get.ts +5 -18
- package/src/cmd/env/import.ts +5 -20
- package/src/cmd/env/list.ts +5 -18
- package/src/cmd/env/pull.ts +10 -23
- package/src/cmd/env/push.ts +5 -23
- package/src/cmd/env/set.ts +5 -20
- package/src/cmd/index.ts +2 -2
- package/src/cmd/profile/show.ts +15 -6
- package/src/cmd/project/create.ts +7 -2
- package/src/cmd/project/delete.ts +75 -18
- package/src/cmd/project/download.ts +2 -2
- package/src/cmd/project/list.ts +8 -8
- package/src/cmd/project/show.ts +3 -7
- package/src/cmd/project/template-flow.ts +170 -72
- package/src/cmd/secret/delete.ts +5 -20
- package/src/cmd/secret/get.ts +5 -18
- package/src/cmd/secret/import.ts +5 -20
- package/src/cmd/secret/list.ts +5 -18
- package/src/cmd/secret/pull.ts +10 -23
- package/src/cmd/secret/push.ts +5 -23
- package/src/cmd/secret/set.ts +5 -20
- package/src/config.ts +224 -24
- package/src/crypto/box.test.ts +431 -0
- package/src/crypto/box.ts +477 -0
- package/src/download.ts +1 -0
- package/src/env-util.test.ts +1 -1
- package/src/steps.ts +65 -6
- package/src/terminal.ts +24 -23
- package/src/tui.ts +192 -61
- package/src/types.ts +291 -201
- package/src/utils/detectSubagent.ts +31 -0
- package/src/utils/zip.ts +38 -0
- package/dist/cmd/example/create-user.d.ts +0 -2
- package/dist/cmd/example/create-user.d.ts.map +0 -1
- package/dist/cmd/example/create.d.ts +0 -2
- package/dist/cmd/example/create.d.ts.map +0 -1
- package/dist/cmd/example/deploy.d.ts +0 -2
- package/dist/cmd/example/deploy.d.ts.map +0 -1
- package/dist/cmd/example/index.d.ts +0 -2
- package/dist/cmd/example/index.d.ts.map +0 -1
- package/dist/cmd/example/list.d.ts +0 -2
- package/dist/cmd/example/list.d.ts.map +0 -1
- package/dist/cmd/example/optional-auth.d.ts +0 -3
- package/dist/cmd/example/optional-auth.d.ts.map +0 -1
- package/dist/cmd/example/run-command.d.ts +0 -2
- package/dist/cmd/example/run-command.d.ts.map +0 -1
- package/dist/cmd/example/sound.d.ts +0 -3
- package/dist/cmd/example/sound.d.ts.map +0 -1
- package/dist/cmd/example/spinner.d.ts +0 -2
- package/dist/cmd/example/spinner.d.ts.map +0 -1
- package/dist/cmd/example/steps.d.ts +0 -2
- package/dist/cmd/example/steps.d.ts.map +0 -1
- package/dist/cmd/example/version.d.ts +0 -2
- package/dist/cmd/example/version.d.ts.map +0 -1
- package/src/cmd/example/create-user.ts +0 -38
- package/src/cmd/example/create.ts +0 -31
- package/src/cmd/example/deploy.ts +0 -36
- package/src/cmd/example/index.ts +0 -29
- package/src/cmd/example/list.ts +0 -32
- package/src/cmd/example/optional-auth.ts +0 -38
- package/src/cmd/example/run-command.ts +0 -45
- package/src/cmd/example/sound.ts +0 -14
- package/src/cmd/example/spinner.ts +0 -44
- package/src/cmd/example/steps.ts +0 -66
- package/src/cmd/example/version.ts +0 -13
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package crypto implements a **FIPS 140-3 compliant KEM-DEM envelope encryption scheme**
|
|
3
|
+
* suitable for multi-gigabyte streams using ECDH P-256 and AES-256-GCM.
|
|
4
|
+
* This design is compatible with the Go implementation and depends only on standard
|
|
5
|
+
* Node.js crypto packages.
|
|
6
|
+
*
|
|
7
|
+
* ────────────────────────── Design summary ─────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* ⚙ KEM (Key-Encapsulation Mechanism)
|
|
10
|
+
* • ECDH P-256 + AES-256-GCM for DEK wrapping
|
|
11
|
+
* • Output: variable-size encrypted DEK (48-byte DEK + 16-byte GCM tag + ephemeral pubkey)
|
|
12
|
+
* • Provides forward secrecy for each blob
|
|
13
|
+
*
|
|
14
|
+
* ⚙ DEM (Data-Encapsulation Mechanism)
|
|
15
|
+
* • AES-256-GCM in ~64 KiB framed chunks (65519 bytes max)
|
|
16
|
+
* • Nonce = 4-byte random prefix ∥ 8-byte little-endian counter
|
|
17
|
+
* • First frame authenticates header via associated data (prevents tampering)
|
|
18
|
+
* • Constant ~64 KiB RAM, O(1) header re-wrap for key rotation
|
|
19
|
+
*
|
|
20
|
+
* ⚙ Fleet key
|
|
21
|
+
* • Single ECDSA P-256 key-pair per customer
|
|
22
|
+
* • Public key used directly for ECDH operations
|
|
23
|
+
* • Private key stored in cloud secret store and fetched at boot
|
|
24
|
+
*
|
|
25
|
+
* File layout
|
|
26
|
+
* ┌─────────────────────────────────────────────────────────────────────────┐
|
|
27
|
+
* │ uint16 wrappedLen │ 125B wrapped DEK │ 12B base nonce │ frames... │
|
|
28
|
+
* └─────────────────────────────────────────────────────────────────────────┘
|
|
29
|
+
* ▲ ▲
|
|
30
|
+
* │ └─ AES-256-GCM frames
|
|
31
|
+
* └─ ECDH + AES-GCM wrapped DEK
|
|
32
|
+
*
|
|
33
|
+
* Security properties
|
|
34
|
+
* • Confidentiality & integrity: AES-256-GCM per frame
|
|
35
|
+
* • Header authentication: first frame includes header as associated data
|
|
36
|
+
* • Forward-secrecy per object: new ephemeral ECDH key each encryption
|
|
37
|
+
* • Key rotation: requires re-wrapping only the ~139-byte header
|
|
38
|
+
* • FIPS 140-3 compliant: uses only approved algorithms
|
|
39
|
+
*
|
|
40
|
+
* Typical workflow
|
|
41
|
+
* ────────────────
|
|
42
|
+
* Publisher:
|
|
43
|
+
* 1) generate DEK, encrypt stream → dst
|
|
44
|
+
* 2) ephemeral ECDH + AES-GCM wrap DEK with fleet public key
|
|
45
|
+
* 3) write header {len, wrapped DEK, nonce} - ~139 bytes total
|
|
46
|
+
* 4) first frame includes header as associated data for authentication
|
|
47
|
+
*
|
|
48
|
+
* Machine node:
|
|
49
|
+
* 1) read header, unwrap DEK with fleet private key via ECDH
|
|
50
|
+
* 2) stream-decrypt frames on the fly (first frame verifies header)
|
|
51
|
+
*
|
|
52
|
+
* Public API
|
|
53
|
+
* ──────────
|
|
54
|
+
*
|
|
55
|
+
* encryptFIPSKEMDEMStream(publicKey: KeyObject, src: Readable, dst: Writable): Promise<number>
|
|
56
|
+
* decryptFIPSKEMDEMStream(privateKey: KeyObject, src: Readable, dst: Writable): Promise<number>
|
|
57
|
+
*
|
|
58
|
+
* Both return the number of plaintext bytes processed and ensure that
|
|
59
|
+
* every error path is authenticated-failure-safe.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import { createCipheriv, createDecipheriv, createECDH, randomBytes, KeyObject } from 'node:crypto';
|
|
63
|
+
import { Readable, Writable } from 'node:stream';
|
|
64
|
+
import { createHash } from 'node:crypto';
|
|
65
|
+
|
|
66
|
+
const FRAME = 65519;
|
|
67
|
+
const DEK_SIZE = 32;
|
|
68
|
+
const GCM_TAG = 16;
|
|
69
|
+
const PUBKEY_LEN = 65;
|
|
70
|
+
|
|
71
|
+
function concatKDFSHA256(z: Buffer, keyDataLen: number, ...otherInfo: Buffer[]): Buffer {
|
|
72
|
+
const h = createHash('sha256');
|
|
73
|
+
h.update(Buffer.from([0x00, 0x00, 0x00, 0x01]));
|
|
74
|
+
h.update(z);
|
|
75
|
+
for (const info of otherInfo) {
|
|
76
|
+
h.update(info);
|
|
77
|
+
}
|
|
78
|
+
const keyDataLenBits = keyDataLen * 8;
|
|
79
|
+
h.update(
|
|
80
|
+
Buffer.from([
|
|
81
|
+
(keyDataLenBits >> 24) & 0xff,
|
|
82
|
+
(keyDataLenBits >> 16) & 0xff,
|
|
83
|
+
(keyDataLenBits >> 8) & 0xff,
|
|
84
|
+
keyDataLenBits & 0xff,
|
|
85
|
+
])
|
|
86
|
+
);
|
|
87
|
+
return h.digest();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function wrapDEKWithECDH(dek: Buffer, recipientPub: KeyObject): Buffer {
|
|
91
|
+
const ephemeral = createECDH('prime256v1');
|
|
92
|
+
ephemeral.generateKeys();
|
|
93
|
+
|
|
94
|
+
const jwk = recipientPub.export({ format: 'jwk' });
|
|
95
|
+
if (!jwk.x || !jwk.y) {
|
|
96
|
+
throw new Error('Invalid EC public key');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const xBuf = Buffer.from(jwk.x, 'base64url');
|
|
100
|
+
const yBuf = Buffer.from(jwk.y, 'base64url');
|
|
101
|
+
const pubKeyPoint = Buffer.concat([Buffer.from([0x04]), xBuf, yBuf]);
|
|
102
|
+
|
|
103
|
+
const sharedSecret = ephemeral.computeSecret(pubKeyPoint);
|
|
104
|
+
const kek = concatKDFSHA256(sharedSecret, 32, Buffer.from('AES-256-GCM'));
|
|
105
|
+
sharedSecret.fill(0);
|
|
106
|
+
|
|
107
|
+
const nonce = randomBytes(12);
|
|
108
|
+
const cipher = createCipheriv('aes-256-gcm', kek, nonce);
|
|
109
|
+
const ciphertext = Buffer.concat([cipher.update(dek), cipher.final()]);
|
|
110
|
+
const tag = cipher.getAuthTag();
|
|
111
|
+
kek.fill(0);
|
|
112
|
+
|
|
113
|
+
const ephemeralPubBytes = ephemeral.getPublicKey(undefined, 'uncompressed');
|
|
114
|
+
return Buffer.concat([ephemeralPubBytes, nonce, ciphertext, tag]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function unwrapDEKWithECDH(wrapped: Buffer, recipientPriv: KeyObject): Buffer {
|
|
118
|
+
if (wrapped.length < PUBKEY_LEN + 12 + DEK_SIZE + GCM_TAG) {
|
|
119
|
+
throw new Error('wrapped DEK too short');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ephemeralPubBytes = wrapped.subarray(0, PUBKEY_LEN);
|
|
123
|
+
const remaining = wrapped.subarray(PUBKEY_LEN);
|
|
124
|
+
|
|
125
|
+
const jwk = recipientPriv.export({ format: 'jwk' });
|
|
126
|
+
if (!jwk.d) {
|
|
127
|
+
throw new Error('Invalid EC private key');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ecdh = createECDH('prime256v1');
|
|
131
|
+
const dBuf = Buffer.from(jwk.d, 'base64url');
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
ecdh.setPrivateKey(dBuf);
|
|
135
|
+
|
|
136
|
+
const sharedSecret = ecdh.computeSecret(ephemeralPubBytes);
|
|
137
|
+
const kek = concatKDFSHA256(sharedSecret, 32, Buffer.from('AES-256-GCM'));
|
|
138
|
+
sharedSecret.fill(0);
|
|
139
|
+
|
|
140
|
+
const nonceSize = 12;
|
|
141
|
+
if (remaining.length < nonceSize) {
|
|
142
|
+
throw new Error('invalid wrapped DEK format');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const nonce = remaining.subarray(0, nonceSize);
|
|
146
|
+
const ciphertextAndTag = remaining.subarray(nonceSize);
|
|
147
|
+
|
|
148
|
+
if (ciphertextAndTag.length < GCM_TAG) {
|
|
149
|
+
throw new Error('invalid wrapped DEK format');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ciphertext = ciphertextAndTag.subarray(0, ciphertextAndTag.length - GCM_TAG);
|
|
153
|
+
const tag = ciphertextAndTag.subarray(ciphertextAndTag.length - GCM_TAG);
|
|
154
|
+
|
|
155
|
+
const decipher = createDecipheriv('aes-256-gcm', kek, nonce);
|
|
156
|
+
decipher.setAuthTag(tag);
|
|
157
|
+
|
|
158
|
+
let plaintext: Buffer;
|
|
159
|
+
try {
|
|
160
|
+
plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
161
|
+
} catch (_err) {
|
|
162
|
+
throw new Error('DEK unwrap failed');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
kek.fill(0);
|
|
166
|
+
return plaintext;
|
|
167
|
+
} finally {
|
|
168
|
+
dBuf.fill(0);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function makeNonce(prefix: Buffer, counter: bigint): Buffer {
|
|
173
|
+
const nonce = Buffer.alloc(12);
|
|
174
|
+
prefix.copy(nonce, 0, 0, 4);
|
|
175
|
+
nonce.writeBigUInt64LE(counter, 4);
|
|
176
|
+
return nonce;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function encryptFIPSKEMDEMStream(
|
|
180
|
+
pub: KeyObject,
|
|
181
|
+
src: Readable,
|
|
182
|
+
dst: Writable
|
|
183
|
+
): Promise<number> {
|
|
184
|
+
if (pub.asymmetricKeyType !== 'ec') {
|
|
185
|
+
throw new Error('only EC keys supported');
|
|
186
|
+
}
|
|
187
|
+
const keyDetails = pub.asymmetricKeyDetails;
|
|
188
|
+
if (!keyDetails || keyDetails.namedCurve !== 'prime256v1') {
|
|
189
|
+
throw new Error('only P-256 keys supported');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const dek = randomBytes(DEK_SIZE);
|
|
193
|
+
let buf: Buffer | undefined;
|
|
194
|
+
const it = src[Symbol.asyncIterator]();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const wrapped = wrapDEKWithECDH(dek, pub);
|
|
198
|
+
|
|
199
|
+
const baseNonce = Buffer.alloc(12);
|
|
200
|
+
randomBytes(4).copy(baseNonce, 0);
|
|
201
|
+
|
|
202
|
+
const lenBuf = Buffer.alloc(2);
|
|
203
|
+
lenBuf.writeUInt16BE(wrapped.length, 0);
|
|
204
|
+
await writeAsync(dst, lenBuf);
|
|
205
|
+
await writeAsync(dst, wrapped);
|
|
206
|
+
await writeAsync(dst, baseNonce);
|
|
207
|
+
|
|
208
|
+
let counter = 0n;
|
|
209
|
+
let total = 0;
|
|
210
|
+
|
|
211
|
+
const headerAD = Buffer.alloc(2 + 12);
|
|
212
|
+
headerAD.writeUInt16BE(wrapped.length, 0);
|
|
213
|
+
baseNonce.copy(headerAD, 2);
|
|
214
|
+
|
|
215
|
+
buf = Buffer.alloc(FRAME);
|
|
216
|
+
|
|
217
|
+
while (true) {
|
|
218
|
+
const bytesRead = await readFull(it, src, buf);
|
|
219
|
+
if (bytesRead === 0) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const plaintext = buf.subarray(0, bytesRead);
|
|
224
|
+
const nonce = makeNonce(baseNonce, counter);
|
|
225
|
+
|
|
226
|
+
const cipher = createCipheriv('aes-256-gcm', dek, nonce);
|
|
227
|
+
|
|
228
|
+
if (counter === 0n) {
|
|
229
|
+
cipher.setAAD(headerAD);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
233
|
+
const tag = cipher.getAuthTag();
|
|
234
|
+
const ct = Buffer.concat([ciphertext, tag]);
|
|
235
|
+
|
|
236
|
+
if (ct.length > 0xffff) {
|
|
237
|
+
throw new Error('ciphertext length exceeds uint16 limit');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const ctLenBuf = Buffer.alloc(2);
|
|
241
|
+
ctLenBuf.writeUInt16BE(ct.length, 0);
|
|
242
|
+
await writeAsync(dst, ctLenBuf);
|
|
243
|
+
await writeAsync(dst, ct);
|
|
244
|
+
|
|
245
|
+
counter++;
|
|
246
|
+
total += bytesRead;
|
|
247
|
+
|
|
248
|
+
if (bytesRead < FRAME) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return total;
|
|
254
|
+
} finally {
|
|
255
|
+
dek.fill(0);
|
|
256
|
+
if (buf) buf.fill(0);
|
|
257
|
+
await it.return?.().catch(() => {});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function decryptFIPSKEMDEMStream(
|
|
262
|
+
priv: KeyObject,
|
|
263
|
+
src: Readable,
|
|
264
|
+
dst: Writable
|
|
265
|
+
): Promise<number> {
|
|
266
|
+
if (priv.asymmetricKeyType !== 'ec') {
|
|
267
|
+
throw new Error('only EC keys supported');
|
|
268
|
+
}
|
|
269
|
+
const keyDetails = priv.asymmetricKeyDetails;
|
|
270
|
+
if (!keyDetails || keyDetails.namedCurve !== 'prime256v1') {
|
|
271
|
+
throw new Error('only P-256 keys supported');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const it = src[Symbol.asyncIterator]();
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const lenBuf = Buffer.alloc(2);
|
|
278
|
+
await readExact(it, src, lenBuf);
|
|
279
|
+
const wrappedLen = lenBuf.readUInt16BE(0);
|
|
280
|
+
|
|
281
|
+
if (wrappedLen === 0 || wrappedLen > 200) {
|
|
282
|
+
throw new Error('invalid wrapped DEK length');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const wrapped = Buffer.alloc(wrappedLen);
|
|
286
|
+
await readExact(it, src, wrapped);
|
|
287
|
+
|
|
288
|
+
const baseNonce = Buffer.alloc(12);
|
|
289
|
+
await readExact(it, src, baseNonce);
|
|
290
|
+
|
|
291
|
+
const dek = unwrapDEKWithECDH(wrapped, priv);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
let counter = 0n;
|
|
295
|
+
let total = 0;
|
|
296
|
+
|
|
297
|
+
const headerAD = Buffer.alloc(2 + 12);
|
|
298
|
+
headerAD.writeUInt16BE(wrappedLen, 0);
|
|
299
|
+
baseNonce.copy(headerAD, 2);
|
|
300
|
+
|
|
301
|
+
while (true) {
|
|
302
|
+
const chunkLenBuf = Buffer.alloc(2);
|
|
303
|
+
const chunkLenRead = await readUpTo(it, src, chunkLenBuf);
|
|
304
|
+
if (chunkLenRead === 0) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
if (chunkLenRead < 2) {
|
|
308
|
+
throw new Error('unexpected EOF reading chunk length');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const chunkLen = chunkLenBuf.readUInt16BE(0);
|
|
312
|
+
if (chunkLen > FRAME + GCM_TAG) {
|
|
313
|
+
throw new Error('chunk too large');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const cipherBuf = Buffer.alloc(chunkLen);
|
|
317
|
+
await readExact(it, src, cipherBuf);
|
|
318
|
+
|
|
319
|
+
if (cipherBuf.length < GCM_TAG) {
|
|
320
|
+
throw new Error('chunk too short for auth tag');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const ciphertext = cipherBuf.subarray(0, cipherBuf.length - GCM_TAG);
|
|
324
|
+
const tag = cipherBuf.subarray(cipherBuf.length - GCM_TAG);
|
|
325
|
+
|
|
326
|
+
const nonce = makeNonce(baseNonce, counter);
|
|
327
|
+
const decipher = createDecipheriv('aes-256-gcm', dek, nonce);
|
|
328
|
+
decipher.setAuthTag(tag);
|
|
329
|
+
|
|
330
|
+
if (counter === 0n) {
|
|
331
|
+
decipher.setAAD(headerAD);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let plain: Buffer;
|
|
335
|
+
try {
|
|
336
|
+
plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
cipherBuf.fill(0);
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
cipherBuf.fill(0);
|
|
343
|
+
|
|
344
|
+
await writeAsync(dst, plain);
|
|
345
|
+
counter++;
|
|
346
|
+
total += plain.length;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return total;
|
|
350
|
+
} finally {
|
|
351
|
+
dek.fill(0);
|
|
352
|
+
}
|
|
353
|
+
} finally {
|
|
354
|
+
await it.return?.().catch(() => {});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function writeAsync(stream: Writable, chunk: Buffer): Promise<void> {
|
|
359
|
+
return new Promise((resolve, reject) => {
|
|
360
|
+
let callbackCompleted = false;
|
|
361
|
+
let drainOccurred = false;
|
|
362
|
+
|
|
363
|
+
const cleanup = () => {
|
|
364
|
+
stream.off('drain', onDrain);
|
|
365
|
+
stream.off('error', onError);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const tryResolve = () => {
|
|
369
|
+
if (callbackCompleted && (canContinue || drainOccurred)) {
|
|
370
|
+
cleanup();
|
|
371
|
+
resolve();
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const onDrain = () => {
|
|
376
|
+
drainOccurred = true;
|
|
377
|
+
tryResolve();
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const onError = (err: Error) => {
|
|
381
|
+
cleanup();
|
|
382
|
+
reject(err);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const canContinue = stream.write(chunk, (err) => {
|
|
386
|
+
callbackCompleted = true;
|
|
387
|
+
if (err) {
|
|
388
|
+
cleanup();
|
|
389
|
+
reject(err);
|
|
390
|
+
} else {
|
|
391
|
+
tryResolve();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (!canContinue) {
|
|
396
|
+
// Need to wait for drain - attach listeners
|
|
397
|
+
stream.once('drain', onDrain);
|
|
398
|
+
stream.once('error', onError);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function readFull(
|
|
404
|
+
iterator: AsyncIterator<Buffer | string>,
|
|
405
|
+
stream: Readable,
|
|
406
|
+
buf: Buffer
|
|
407
|
+
): Promise<number> {
|
|
408
|
+
let offset = 0;
|
|
409
|
+
|
|
410
|
+
while (offset < buf.length) {
|
|
411
|
+
const result = await iterator.next();
|
|
412
|
+
if (result.done) {
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const chunk = result.value;
|
|
417
|
+
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
418
|
+
const toCopy = Math.min(chunkBuf.length, buf.length - offset);
|
|
419
|
+
chunkBuf.copy(buf, offset, 0, toCopy);
|
|
420
|
+
offset += toCopy;
|
|
421
|
+
|
|
422
|
+
if (offset >= buf.length && toCopy < chunkBuf.length) {
|
|
423
|
+
stream.unshift(chunkBuf.subarray(toCopy));
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return offset;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function readExact(
|
|
432
|
+
iterator: AsyncIterator<Buffer | string>,
|
|
433
|
+
stream: Readable,
|
|
434
|
+
buf: Buffer
|
|
435
|
+
): Promise<void> {
|
|
436
|
+
let offset = 0;
|
|
437
|
+
|
|
438
|
+
while (offset < buf.length) {
|
|
439
|
+
const result = await iterator.next();
|
|
440
|
+
if (result.done) {
|
|
441
|
+
throw new Error('unexpected EOF');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const chunk = result.value;
|
|
445
|
+
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
446
|
+
const toCopy = Math.min(chunkBuf.length, buf.length - offset);
|
|
447
|
+
chunkBuf.copy(buf, offset, 0, toCopy);
|
|
448
|
+
offset += toCopy;
|
|
449
|
+
|
|
450
|
+
if (offset >= buf.length && toCopy < chunkBuf.length) {
|
|
451
|
+
stream.unshift(chunkBuf.subarray(toCopy));
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function readUpTo(
|
|
458
|
+
iterator: AsyncIterator<Buffer | string>,
|
|
459
|
+
stream: Readable,
|
|
460
|
+
buf: Buffer
|
|
461
|
+
): Promise<number> {
|
|
462
|
+
const result = await iterator.next();
|
|
463
|
+
if (result.done) {
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const chunk = result.value;
|
|
468
|
+
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
469
|
+
const toCopy = Math.min(chunkBuf.length, buf.length);
|
|
470
|
+
chunkBuf.copy(buf, 0, 0, toCopy);
|
|
471
|
+
|
|
472
|
+
if (toCopy < chunkBuf.length) {
|
|
473
|
+
stream.unshift(chunkBuf.subarray(toCopy));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return toCopy;
|
|
477
|
+
}
|
package/src/download.ts
CHANGED
package/src/env-util.test.ts
CHANGED
|
@@ -122,7 +122,7 @@ describe('looksLikeSecret', () => {
|
|
|
122
122
|
describe('non-secret patterns', () => {
|
|
123
123
|
test('regular environment variables are not flagged', () => {
|
|
124
124
|
expect(looksLikeSecret('NODE_ENV', 'production')).toBe(false);
|
|
125
|
-
expect(looksLikeSecret('PORT', '
|
|
125
|
+
expect(looksLikeSecret('PORT', '3500')).toBe(false);
|
|
126
126
|
expect(looksLikeSecret('HOST', 'localhost')).toBe(false);
|
|
127
127
|
expect(looksLikeSecret('DATABASE_URL', 'postgres://localhost:5432/mydb')).toBe(false);
|
|
128
128
|
});
|
package/src/steps.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ColorScheme } from './terminal';
|
|
9
|
+
import type { LogLevel } from './types';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Get the appropriate exit function (Bun.exit or process.exit)
|
|
@@ -169,8 +170,10 @@ type StepState =
|
|
|
169
170
|
* Each step runs its callback while showing a spinner animation.
|
|
170
171
|
* Steps can complete with success, skipped, or error status.
|
|
171
172
|
* Exits with code 1 if any step errors.
|
|
173
|
+
*
|
|
174
|
+
* When there's no TTY or log level is debug/trace, uses plain output instead of TUI.
|
|
172
175
|
*/
|
|
173
|
-
export async function runSteps(steps: Step[]): Promise<void> {
|
|
176
|
+
export async function runSteps(steps: Step[], logLevel?: LogLevel): Promise<void> {
|
|
174
177
|
const state: StepState[] = steps.map((s) => {
|
|
175
178
|
const stepType = s.type === 'progress' ? 'progress' : 'simple';
|
|
176
179
|
return stepType === 'progress'
|
|
@@ -182,6 +185,21 @@ export async function runSteps(steps: Step[]): Promise<void> {
|
|
|
182
185
|
: { type: 'simple' as const, label: s.label, run: s.run as () => Promise<StepOutcome> };
|
|
183
186
|
});
|
|
184
187
|
|
|
188
|
+
// Detect if we should use TUI (animated) or plain mode
|
|
189
|
+
const useTUI =
|
|
190
|
+
process.stdout.isTTY && (!logLevel || ['info', 'warn', 'error'].includes(logLevel));
|
|
191
|
+
|
|
192
|
+
if (useTUI) {
|
|
193
|
+
await runStepsTUI(state);
|
|
194
|
+
} else {
|
|
195
|
+
await runStepsPlain(state);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run steps with animated TUI (original behavior)
|
|
201
|
+
*/
|
|
202
|
+
async function runStepsTUI(state: StepState[]): Promise<void> {
|
|
185
203
|
// Hide cursor
|
|
186
204
|
process.stdout.write('\x1B[?25l');
|
|
187
205
|
|
|
@@ -209,16 +227,17 @@ export async function runSteps(steps: Step[]): Promise<void> {
|
|
|
209
227
|
|
|
210
228
|
const step = state[stepIndex];
|
|
211
229
|
let frameIndex = 0;
|
|
230
|
+
let currentFrame = '';
|
|
212
231
|
|
|
213
232
|
// Start spinner animation
|
|
214
233
|
activeInterval = setInterval(() => {
|
|
215
234
|
const colorKey = SPINNER_COLORS[frameIndex % SPINNER_COLORS.length];
|
|
216
235
|
const color = getColor(colorKey);
|
|
217
|
-
|
|
236
|
+
currentFrame = `${color}${COLORS.bold}${FRAMES[frameIndex % FRAMES.length]}${COLORS.reset}`;
|
|
218
237
|
|
|
219
238
|
// Move cursor up to the top of checklist
|
|
220
239
|
process.stdout.write(`\x1B[${state.length}A`);
|
|
221
|
-
process.stdout.write(renderSteps(state, stepIndex,
|
|
240
|
+
process.stdout.write(renderSteps(state, stepIndex, currentFrame) + '\n');
|
|
222
241
|
|
|
223
242
|
frameIndex++;
|
|
224
243
|
}, 120);
|
|
@@ -227,9 +246,9 @@ export async function runSteps(steps: Step[]): Promise<void> {
|
|
|
227
246
|
const progressCallback: ProgressCallback = (progress: number) => {
|
|
228
247
|
step.progress = Math.min(100, Math.max(0, progress));
|
|
229
248
|
|
|
230
|
-
// Move cursor up
|
|
249
|
+
// Move cursor up and render with current spinner frame
|
|
231
250
|
process.stdout.write(`\x1B[${state.length}A`);
|
|
232
|
-
process.stdout.write(renderSteps(state, stepIndex) + '\n');
|
|
251
|
+
process.stdout.write(renderSteps(state, stepIndex, currentFrame) + '\n');
|
|
233
252
|
};
|
|
234
253
|
|
|
235
254
|
try {
|
|
@@ -252,7 +271,7 @@ export async function runSteps(steps: Step[]): Promise<void> {
|
|
|
252
271
|
// Clear progress and final render with outcome
|
|
253
272
|
step.progress = undefined;
|
|
254
273
|
process.stdout.write(`\x1B[${state.length}A`);
|
|
255
|
-
process.stdout.write(renderSteps(state,
|
|
274
|
+
process.stdout.write(renderSteps(state, -1) + '\n');
|
|
256
275
|
|
|
257
276
|
// If error, show error message and exit
|
|
258
277
|
if (step.outcome?.status === 'error') {
|
|
@@ -275,6 +294,44 @@ export async function runSteps(steps: Step[]): Promise<void> {
|
|
|
275
294
|
}
|
|
276
295
|
}
|
|
277
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Run steps in plain mode (no TUI animations)
|
|
299
|
+
*/
|
|
300
|
+
async function runStepsPlain(state: StepState[]): Promise<void> {
|
|
301
|
+
const grayColor = getColor('gray');
|
|
302
|
+
const greenColor = getColor('green');
|
|
303
|
+
const yellowColor = getColor('yellow');
|
|
304
|
+
const redColor = getColor('red');
|
|
305
|
+
|
|
306
|
+
for (const step of state) {
|
|
307
|
+
// Run the step (no progress callback for plain mode)
|
|
308
|
+
try {
|
|
309
|
+
const outcome = step.type === 'progress' ? await step.run(() => {}) : await step.run();
|
|
310
|
+
step.outcome = outcome;
|
|
311
|
+
} catch (err) {
|
|
312
|
+
step.outcome = {
|
|
313
|
+
status: 'error',
|
|
314
|
+
message: err instanceof Error ? err.message : String(err),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Print final state only
|
|
319
|
+
if (step.outcome?.status === 'success') {
|
|
320
|
+
console.log(`${greenColor}${ICONS.success}${COLORS.reset} ${step.label}`);
|
|
321
|
+
} else if (step.outcome?.status === 'skipped') {
|
|
322
|
+
const reason = step.outcome.reason
|
|
323
|
+
? ` ${grayColor}(${step.outcome.reason})${COLORS.reset}`
|
|
324
|
+
: '';
|
|
325
|
+
console.log(`${yellowColor}${ICONS.skipped}${COLORS.reset} ${step.label}${reason}`);
|
|
326
|
+
} else if (step.outcome?.status === 'error') {
|
|
327
|
+
console.log(`${redColor}${ICONS.error}${COLORS.reset} ${step.label}`);
|
|
328
|
+
const errorColor = getColor('red');
|
|
329
|
+
console.error(`\n${errorColor}Error: ${step.outcome.message}${COLORS.reset}\n`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
278
335
|
/**
|
|
279
336
|
* Render a progress indicator
|
|
280
337
|
*/
|
|
@@ -297,6 +354,7 @@ function renderSteps(steps: StepState[], activeIndex: number, spinner?: string):
|
|
|
297
354
|
const lines: string[] = [];
|
|
298
355
|
|
|
299
356
|
steps.forEach((s, i) => {
|
|
357
|
+
// Don't show progress indicator for steps with outcomes (success/skipped/error)
|
|
300
358
|
if (s.outcome?.status === 'success') {
|
|
301
359
|
lines.push(
|
|
302
360
|
`${greenColor}${ICONS.success}${COLORS.reset} ${grayColor}${COLORS.strikethrough}${s.label}${COLORS.reset}`
|
|
@@ -309,6 +367,7 @@ function renderSteps(steps: StepState[], activeIndex: number, spinner?: string):
|
|
|
309
367
|
} else if (s.outcome?.status === 'error') {
|
|
310
368
|
lines.push(`${redColor}${ICONS.error}${COLORS.reset} ${s.label}`);
|
|
311
369
|
} else if (i === activeIndex && spinner) {
|
|
370
|
+
// Only show progress for active step with spinner
|
|
312
371
|
const progressIndicator = s.progress !== undefined ? renderProgress(s.progress) : '';
|
|
313
372
|
lines.push(`${spinner} ${s.label}${progressIndicator}`);
|
|
314
373
|
} else {
|
package/src/terminal.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type ColorScheme = 'light' | 'dark';
|
|
2
2
|
|
|
3
|
+
const defaultMode = process.env.CI ? 'light' : 'dark';
|
|
4
|
+
|
|
3
5
|
export async function detectColorScheme(): Promise<ColorScheme> {
|
|
4
6
|
const debug = process.env.DEBUG_COLORS === 'true';
|
|
5
7
|
|
|
@@ -14,28 +16,27 @@ export async function detectColorScheme(): Promise<ColorScheme> {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
// Check if we have stdout TTY at minimum
|
|
17
|
-
if (
|
|
18
|
-
if (debug) console.log('[DEBUG] stdout
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
if (process.stdout.isTTY) {
|
|
20
|
+
if (debug) console.log('[DEBUG] stdout is a TTY, defaulting to dark');
|
|
21
|
+
|
|
22
|
+
// Try to query terminal background color using OSC 11 (most reliable)
|
|
23
|
+
if (debug) console.log('[DEBUG] Querying terminal background with OSC 11...');
|
|
24
|
+
try {
|
|
25
|
+
const bgColor = await queryTerminalBackground();
|
|
26
|
+
if (bgColor) {
|
|
27
|
+
const luminance = calculateLuminance(bgColor);
|
|
28
|
+
const scheme = luminance > 0.5 ? 'light' : 'dark';
|
|
29
|
+
if (debug)
|
|
30
|
+
console.log(
|
|
31
|
+
`[DEBUG] OSC 11 response: rgb(${bgColor.r},${bgColor.g},${bgColor.b}), luminance: ${luminance.toFixed(2)}, scheme: ${scheme}`
|
|
32
|
+
);
|
|
33
|
+
return scheme;
|
|
34
|
+
} else {
|
|
35
|
+
if (debug) console.log('[DEBUG] OSC 11 query timed out or no response');
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (debug) console.log('[DEBUG] OSC 11 query failed:', error);
|
|
36
39
|
}
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if (debug) console.log('[DEBUG] OSC 11 query failed:', error);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// Fall back to COLORFGBG environment variable (less reliable)
|
|
@@ -56,8 +57,8 @@ export async function detectColorScheme(): Promise<ColorScheme> {
|
|
|
56
57
|
return scheme;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
if (debug) console.log('[DEBUG] Defaulting to
|
|
60
|
-
return
|
|
60
|
+
if (debug) console.log('[DEBUG] Defaulting to %s', defaultMode);
|
|
61
|
+
return defaultMode;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
interface RGBColor {
|