@agentuity/cli 0.0.43 → 0.0.45

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.
Files changed (209) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/api.d.ts +3 -3
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/auth.d.ts +10 -2
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/banner.d.ts.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cmd/auth/api.d.ts +4 -4
  10. package/dist/cmd/auth/api.d.ts.map +1 -1
  11. package/dist/cmd/auth/index.d.ts.map +1 -1
  12. package/dist/cmd/auth/login.d.ts.map +1 -1
  13. package/dist/cmd/auth/signup.d.ts.map +1 -1
  14. package/dist/cmd/auth/ssh/add.d.ts +2 -0
  15. package/dist/cmd/auth/ssh/add.d.ts.map +1 -0
  16. package/dist/cmd/auth/ssh/api.d.ts +16 -0
  17. package/dist/cmd/auth/ssh/api.d.ts.map +1 -0
  18. package/dist/cmd/auth/ssh/delete.d.ts +2 -0
  19. package/dist/cmd/auth/ssh/delete.d.ts.map +1 -0
  20. package/dist/cmd/auth/ssh/index.d.ts +3 -0
  21. package/dist/cmd/auth/ssh/index.d.ts.map +1 -0
  22. package/dist/cmd/auth/ssh/list.d.ts +2 -0
  23. package/dist/cmd/auth/ssh/list.d.ts.map +1 -0
  24. package/dist/cmd/auth/whoami.d.ts.map +1 -1
  25. package/dist/cmd/bundle/ast.d.ts +14 -3
  26. package/dist/cmd/bundle/ast.d.ts.map +1 -1
  27. package/dist/cmd/bundle/ast.test.d.ts +2 -0
  28. package/dist/cmd/bundle/ast.test.d.ts.map +1 -0
  29. package/dist/cmd/bundle/bundler.d.ts +6 -1
  30. package/dist/cmd/bundle/bundler.d.ts.map +1 -1
  31. package/dist/cmd/bundle/file.d.ts.map +1 -1
  32. package/dist/cmd/bundle/fix-duplicate-exports.d.ts +2 -0
  33. package/dist/cmd/bundle/fix-duplicate-exports.d.ts.map +1 -0
  34. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts +2 -0
  35. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts.map +1 -0
  36. package/dist/cmd/bundle/plugin.d.ts +2 -0
  37. package/dist/cmd/bundle/plugin.d.ts.map +1 -1
  38. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  39. package/dist/cmd/cloud/domain.d.ts +17 -0
  40. package/dist/cmd/cloud/domain.d.ts.map +1 -0
  41. package/dist/cmd/cloud/index.d.ts.map +1 -1
  42. package/dist/cmd/cloud/resource/add.d.ts +2 -0
  43. package/dist/cmd/cloud/resource/add.d.ts.map +1 -0
  44. package/dist/cmd/cloud/resource/delete.d.ts +2 -0
  45. package/dist/cmd/cloud/resource/delete.d.ts.map +1 -0
  46. package/dist/cmd/cloud/resource/index.d.ts +3 -0
  47. package/dist/cmd/cloud/resource/index.d.ts.map +1 -0
  48. package/dist/cmd/cloud/resource/list.d.ts +2 -0
  49. package/dist/cmd/cloud/resource/list.d.ts.map +1 -0
  50. package/dist/cmd/cloud/scp/download.d.ts +2 -0
  51. package/dist/cmd/cloud/scp/download.d.ts.map +1 -0
  52. package/dist/cmd/cloud/scp/index.d.ts +3 -0
  53. package/dist/cmd/cloud/scp/index.d.ts.map +1 -0
  54. package/dist/cmd/cloud/scp/upload.d.ts +2 -0
  55. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -0
  56. package/dist/cmd/cloud/ssh.d.ts +2 -0
  57. package/dist/cmd/cloud/ssh.d.ts.map +1 -0
  58. package/dist/cmd/dev/api.d.ts +18 -0
  59. package/dist/cmd/dev/api.d.ts.map +1 -0
  60. package/dist/cmd/dev/download.d.ts +11 -0
  61. package/dist/cmd/dev/download.d.ts.map +1 -0
  62. package/dist/cmd/dev/index.d.ts.map +1 -1
  63. package/dist/cmd/dev/templates.d.ts +3 -0
  64. package/dist/cmd/dev/templates.d.ts.map +1 -0
  65. package/dist/cmd/env/delete.d.ts.map +1 -1
  66. package/dist/cmd/env/get.d.ts.map +1 -1
  67. package/dist/cmd/env/import.d.ts.map +1 -1
  68. package/dist/cmd/env/list.d.ts.map +1 -1
  69. package/dist/cmd/env/pull.d.ts.map +1 -1
  70. package/dist/cmd/env/push.d.ts.map +1 -1
  71. package/dist/cmd/env/set.d.ts.map +1 -1
  72. package/dist/cmd/profile/show.d.ts.map +1 -1
  73. package/dist/cmd/project/create.d.ts.map +1 -1
  74. package/dist/cmd/project/delete.d.ts.map +1 -1
  75. package/dist/cmd/project/list.d.ts.map +1 -1
  76. package/dist/cmd/project/show.d.ts.map +1 -1
  77. package/dist/cmd/project/template-flow.d.ts +4 -0
  78. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  79. package/dist/cmd/secret/delete.d.ts.map +1 -1
  80. package/dist/cmd/secret/get.d.ts.map +1 -1
  81. package/dist/cmd/secret/import.d.ts.map +1 -1
  82. package/dist/cmd/secret/list.d.ts.map +1 -1
  83. package/dist/cmd/secret/pull.d.ts.map +1 -1
  84. package/dist/cmd/secret/push.d.ts.map +1 -1
  85. package/dist/cmd/secret/set.d.ts.map +1 -1
  86. package/dist/config.d.ts +9 -3
  87. package/dist/config.d.ts.map +1 -1
  88. package/dist/crypto/box.d.ts +65 -0
  89. package/dist/crypto/box.d.ts.map +1 -0
  90. package/dist/crypto/box.test.d.ts +2 -0
  91. package/dist/crypto/box.test.d.ts.map +1 -0
  92. package/dist/download.d.ts.map +1 -1
  93. package/dist/steps.d.ts +4 -1
  94. package/dist/steps.d.ts.map +1 -1
  95. package/dist/terminal.d.ts.map +1 -1
  96. package/dist/tui.d.ts +31 -1
  97. package/dist/tui.d.ts.map +1 -1
  98. package/dist/types.d.ts +249 -126
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/utils/detectSubagent.d.ts +15 -0
  101. package/dist/utils/detectSubagent.d.ts.map +1 -0
  102. package/dist/utils/zip.d.ts +7 -0
  103. package/dist/utils/zip.d.ts.map +1 -0
  104. package/package.json +11 -3
  105. package/src/api-errors.md +2 -2
  106. package/src/api.ts +12 -7
  107. package/src/auth.ts +116 -7
  108. package/src/banner.ts +13 -6
  109. package/src/cli.ts +695 -63
  110. package/src/cmd/auth/api.ts +10 -16
  111. package/src/cmd/auth/index.ts +2 -1
  112. package/src/cmd/auth/login.ts +24 -8
  113. package/src/cmd/auth/signup.ts +15 -11
  114. package/src/cmd/auth/ssh/add.ts +263 -0
  115. package/src/cmd/auth/ssh/api.ts +94 -0
  116. package/src/cmd/auth/ssh/delete.ts +102 -0
  117. package/src/cmd/auth/ssh/index.ts +10 -0
  118. package/src/cmd/auth/ssh/list.ts +74 -0
  119. package/src/cmd/auth/whoami.ts +13 -13
  120. package/src/cmd/bundle/ast.test.ts +565 -0
  121. package/src/cmd/bundle/ast.ts +457 -44
  122. package/src/cmd/bundle/bundler.ts +255 -57
  123. package/src/cmd/bundle/file.ts +6 -12
  124. package/src/cmd/bundle/fix-duplicate-exports.test.ts +387 -0
  125. package/src/cmd/bundle/fix-duplicate-exports.ts +204 -0
  126. package/src/cmd/bundle/index.ts +9 -9
  127. package/src/cmd/bundle/patch/aisdk.ts +1 -1
  128. package/src/cmd/bundle/plugin.ts +373 -53
  129. package/src/cmd/cloud/deploy.ts +300 -93
  130. package/src/cmd/cloud/domain.ts +92 -0
  131. package/src/cmd/cloud/index.ts +4 -1
  132. package/src/cmd/cloud/resource/add.ts +56 -0
  133. package/src/cmd/cloud/resource/delete.ts +120 -0
  134. package/src/cmd/cloud/resource/index.ts +11 -0
  135. package/src/cmd/cloud/resource/list.ts +69 -0
  136. package/src/cmd/cloud/scp/download.ts +59 -0
  137. package/src/cmd/cloud/scp/index.ts +9 -0
  138. package/src/cmd/cloud/scp/upload.ts +62 -0
  139. package/src/cmd/cloud/ssh.ts +68 -0
  140. package/src/cmd/dev/api.ts +46 -0
  141. package/src/cmd/dev/download.ts +111 -0
  142. package/src/cmd/dev/index.ts +360 -34
  143. package/src/cmd/dev/templates.ts +84 -0
  144. package/src/cmd/env/delete.ts +5 -20
  145. package/src/cmd/env/get.ts +5 -18
  146. package/src/cmd/env/import.ts +5 -20
  147. package/src/cmd/env/list.ts +5 -18
  148. package/src/cmd/env/pull.ts +10 -23
  149. package/src/cmd/env/push.ts +5 -23
  150. package/src/cmd/env/set.ts +5 -20
  151. package/src/cmd/index.ts +2 -2
  152. package/src/cmd/profile/show.ts +15 -6
  153. package/src/cmd/project/create.ts +7 -2
  154. package/src/cmd/project/delete.ts +75 -18
  155. package/src/cmd/project/download.ts +2 -2
  156. package/src/cmd/project/list.ts +8 -8
  157. package/src/cmd/project/show.ts +3 -7
  158. package/src/cmd/project/template-flow.ts +170 -72
  159. package/src/cmd/secret/delete.ts +5 -20
  160. package/src/cmd/secret/get.ts +5 -18
  161. package/src/cmd/secret/import.ts +5 -20
  162. package/src/cmd/secret/list.ts +5 -18
  163. package/src/cmd/secret/pull.ts +10 -23
  164. package/src/cmd/secret/push.ts +5 -23
  165. package/src/cmd/secret/set.ts +5 -20
  166. package/src/config.ts +224 -24
  167. package/src/crypto/box.test.ts +431 -0
  168. package/src/crypto/box.ts +477 -0
  169. package/src/download.ts +1 -0
  170. package/src/env-util.test.ts +1 -1
  171. package/src/steps.ts +65 -6
  172. package/src/terminal.ts +24 -23
  173. package/src/tui.ts +192 -61
  174. package/src/types.ts +291 -201
  175. package/src/utils/detectSubagent.ts +31 -0
  176. package/src/utils/zip.ts +38 -0
  177. package/dist/cmd/example/create-user.d.ts +0 -2
  178. package/dist/cmd/example/create-user.d.ts.map +0 -1
  179. package/dist/cmd/example/create.d.ts +0 -2
  180. package/dist/cmd/example/create.d.ts.map +0 -1
  181. package/dist/cmd/example/deploy.d.ts +0 -2
  182. package/dist/cmd/example/deploy.d.ts.map +0 -1
  183. package/dist/cmd/example/index.d.ts +0 -2
  184. package/dist/cmd/example/index.d.ts.map +0 -1
  185. package/dist/cmd/example/list.d.ts +0 -2
  186. package/dist/cmd/example/list.d.ts.map +0 -1
  187. package/dist/cmd/example/optional-auth.d.ts +0 -3
  188. package/dist/cmd/example/optional-auth.d.ts.map +0 -1
  189. package/dist/cmd/example/run-command.d.ts +0 -2
  190. package/dist/cmd/example/run-command.d.ts.map +0 -1
  191. package/dist/cmd/example/sound.d.ts +0 -3
  192. package/dist/cmd/example/sound.d.ts.map +0 -1
  193. package/dist/cmd/example/spinner.d.ts +0 -2
  194. package/dist/cmd/example/spinner.d.ts.map +0 -1
  195. package/dist/cmd/example/steps.d.ts +0 -2
  196. package/dist/cmd/example/steps.d.ts.map +0 -1
  197. package/dist/cmd/example/version.d.ts +0 -2
  198. package/dist/cmd/example/version.d.ts.map +0 -1
  199. package/src/cmd/example/create-user.ts +0 -38
  200. package/src/cmd/example/create.ts +0 -31
  201. package/src/cmd/example/deploy.ts +0 -36
  202. package/src/cmd/example/index.ts +0 -29
  203. package/src/cmd/example/list.ts +0 -32
  204. package/src/cmd/example/optional-auth.ts +0 -38
  205. package/src/cmd/example/run-command.ts +0 -45
  206. package/src/cmd/example/sound.ts +0 -14
  207. package/src/cmd/example/spinner.ts +0 -44
  208. package/src/cmd/example/steps.ts +0 -66
  209. 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
@@ -66,6 +66,7 @@ export async function downloadWithSpinner<T>(
66
66
  return await tui.spinner({
67
67
  type: 'progress',
68
68
  message,
69
+ clearOnSuccess: true,
69
70
  callback: async (updateProgress) => {
70
71
  const stream = await downloadWithProgress({
71
72
  ...options,
@@ -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', '3000')).toBe(false);
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
- const frame = `${color}${COLORS.bold}${FRAMES[frameIndex % FRAMES.length]}${COLORS.reset}`;
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, frame) + '\n');
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, stepIndex) + '\n');
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 (!process.stdout.isTTY) {
18
- if (debug) console.log('[DEBUG] stdout not a TTY, defaulting to dark');
19
- return 'dark'; // Default to dark mode
20
- }
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');
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 dark mode');
60
- return 'dark'; // Default to dark mode
60
+ if (debug) console.log('[DEBUG] Defaulting to %s', defaultMode);
61
+ return defaultMode;
61
62
  }
62
63
 
63
64
  interface RGBColor {