t3code-cli 0.11.0 → 0.12.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.
Files changed (317) hide show
  1. package/README.md +42 -10
  2. package/dist/application.js +2 -2
  3. package/dist/auth.js +1 -1
  4. package/dist/bin.js +85526 -2662
  5. package/dist/cli.js +15 -2
  6. package/dist/config.js +120 -2
  7. package/dist/connection.js +2 -2
  8. package/dist/index.js +1 -1
  9. package/dist/node.js +6 -2
  10. package/dist/orchestration.js +2 -2
  11. package/dist/rpc.js +1 -1
  12. package/dist/runtime.js +2 -2
  13. package/dist/shared.js +42956 -38119
  14. package/dist/src/application/actions.d.ts +25 -0
  15. package/dist/src/application/index.d.ts +3 -3
  16. package/dist/src/application/layer.d.ts +51 -35
  17. package/dist/src/application/model-selection.d.ts +2 -2
  18. package/dist/src/application/models.d.ts +1 -1
  19. package/dist/src/application/project-commands.d.ts +22 -2
  20. package/dist/src/application/projects.d.ts +7 -7
  21. package/dist/src/application/service.d.ts +65 -2
  22. package/dist/src/application/shell-sequence.d.ts +11 -0
  23. package/dist/src/application/terminals.d.ts +2 -2
  24. package/dist/src/application/thread-commands.d.ts +1 -1
  25. package/dist/src/application/thread-wait.d.ts +9 -0
  26. package/dist/src/application/threads.d.ts +56 -5
  27. package/dist/src/auth/error.d.ts +2 -1
  28. package/dist/src/auth/index.d.ts +1 -1
  29. package/dist/src/auth/layer.d.ts +34 -6
  30. package/dist/src/auth/local-base-dir.d.ts +5 -2
  31. package/dist/src/auth/local-origin.d.ts +3 -3
  32. package/dist/src/auth/local-token.d.ts +3 -3
  33. package/dist/src/auth/service.d.ts +15 -3
  34. package/dist/src/auth/type.d.ts +40 -1
  35. package/dist/src/cli/env/flag.d.ts +2 -0
  36. package/dist/src/cli/env/index.d.ts +2 -0
  37. package/dist/src/cli/env/selection-layer.d.ts +3 -0
  38. package/dist/src/cli/flags.d.ts +2 -0
  39. package/dist/src/cli/format/index.d.ts +1 -0
  40. package/dist/src/cli/format/output.d.ts +12 -0
  41. package/dist/src/cli/index.d.ts +4 -0
  42. package/dist/src/cli/runtime/index.d.ts +1 -0
  43. package/dist/src/cli/runtime/service.d.ts +16 -0
  44. package/dist/src/{scope → cli/scope}/resolve.d.ts +5 -4
  45. package/dist/src/config/config.d.ts +49 -0
  46. package/dist/src/config/credential/cipher-node.d.ts +3 -0
  47. package/dist/src/config/credential/cipher-web.d.ts +3 -0
  48. package/dist/src/config/credential/cipher.d.ts +29 -0
  49. package/dist/src/config/credential/env.d.ts +3 -0
  50. package/dist/src/config/credential/error.d.ts +8 -0
  51. package/dist/src/config/credential/index.d.ts +6 -0
  52. package/dist/src/config/credential/service.d.ts +34 -0
  53. package/dist/src/config/env/env.d.ts +55 -0
  54. package/dist/src/config/env/index.d.ts +2 -0
  55. package/dist/src/{layout/base-dir.d.ts → config/env/layout.d.ts} +0 -1
  56. package/dist/src/config/environment-name/index.d.ts +1 -0
  57. package/dist/src/config/environment-name/name.d.ts +10 -0
  58. package/dist/src/config/error.d.ts +4 -11
  59. package/dist/src/config/index.d.ts +10 -5
  60. package/dist/src/config/keystore/error.d.ts +8 -0
  61. package/dist/src/config/keystore/file.d.ts +9 -0
  62. package/dist/src/config/keystore/index.d.ts +4 -0
  63. package/dist/src/config/keystore/keyring-node.d.ts +4 -0
  64. package/dist/src/config/keystore/service.d.ts +33 -0
  65. package/dist/src/config/paths/index.d.ts +1 -0
  66. package/dist/src/config/paths/paths.d.ts +6 -0
  67. package/dist/src/config/persist/file-mode.d.ts +4 -0
  68. package/dist/src/config/persist/migration.d.ts +68 -0
  69. package/dist/src/config/persist/persist.d.ts +40 -0
  70. package/dist/src/config/persist/schema.d.ts +68 -0
  71. package/dist/src/config/resolve/resolve.d.ts +40 -0
  72. package/dist/src/config/selection/index.d.ts +1 -0
  73. package/dist/src/config/selection/resolve.d.ts +4 -0
  74. package/dist/src/config/selection/service.d.ts +10 -0
  75. package/dist/src/config/types.d.ts +36 -0
  76. package/dist/src/config/url/error.d.ts +10 -0
  77. package/dist/src/config/url/index.d.ts +2 -0
  78. package/dist/src/connection/index.d.ts +0 -1
  79. package/dist/src/contracts/index.d.ts +1 -1
  80. package/dist/src/domain/error.d.ts +14 -1
  81. package/dist/src/domain/helpers.d.ts +16 -1
  82. package/dist/src/domain/model-config.d.ts +4 -1
  83. package/dist/src/domain/thread-activities.d.ts +1 -1
  84. package/dist/src/domain/thread-lifecycle.d.ts +10 -1
  85. package/dist/src/index.d.ts +1 -2
  86. package/dist/src/{connection/node.d.ts → node/connection.d.ts} +1 -1
  87. package/dist/src/node/index.d.ts +1 -2
  88. package/dist/src/orchestration/index.d.ts +0 -1
  89. package/dist/src/orchestration/layer.d.ts +153 -7
  90. package/dist/src/orchestration/service.d.ts +1 -1
  91. package/dist/src/rpc/error.d.ts +2 -2
  92. package/dist/src/rpc/index.d.ts +2 -3
  93. package/dist/src/rpc/layer.d.ts +574 -46
  94. package/dist/src/rpc/operation.d.ts +574 -46
  95. package/dist/src/rpc/ws-group.d.ts +184 -19
  96. package/dist/src/runtime/index.d.ts +1 -2
  97. package/dist/src/runtime/layer.d.ts +12 -8
  98. package/dist/src/t3tools/index.d.ts +1 -1
  99. package/dist/t3tools.js +2 -2
  100. package/package.json +25 -21
  101. package/src/application/actions.test.ts +328 -0
  102. package/src/application/actions.ts +475 -0
  103. package/src/application/index.ts +9 -5
  104. package/src/application/layer.ts +9 -0
  105. package/src/application/model-selection.ts +2 -2
  106. package/src/application/project-commands.ts +23 -5
  107. package/src/application/projects.ts +6 -7
  108. package/src/application/service.ts +79 -1
  109. package/src/application/shell-sequence.ts +1 -1
  110. package/src/application/terminals.ts +2 -1
  111. package/src/application/thread-commands.ts +1 -1
  112. package/src/application/thread-update.ts +1 -1
  113. package/src/application/thread-wait.ts +1 -1
  114. package/src/application/threads.test.ts +9 -3
  115. package/src/application/threads.ts +6 -6
  116. package/src/auth/error.ts +2 -1
  117. package/src/auth/index.ts +7 -0
  118. package/src/auth/layer.test-utils.ts +34 -0
  119. package/src/auth/layer.ts +116 -34
  120. package/src/auth/local-base-dir.ts +13 -6
  121. package/src/auth/local-origin.ts +10 -7
  122. package/src/auth/local-token.ts +28 -23
  123. package/src/auth/service.test.ts +200 -0
  124. package/src/auth/service.ts +27 -3
  125. package/src/auth/transport.ts +2 -2
  126. package/src/auth/type.ts +47 -1
  127. package/src/bin.ts +24 -6
  128. package/src/cli/action-format.ts +110 -0
  129. package/src/cli/action.ts +398 -0
  130. package/src/cli/app.ts +6 -0
  131. package/src/cli/auth.ts +76 -23
  132. package/src/cli/env/flag.ts +8 -0
  133. package/src/cli/env/index.ts +2 -0
  134. package/src/cli/env/selection-layer.test.ts +49 -0
  135. package/src/cli/env/selection-layer.ts +27 -0
  136. package/src/cli/env.ts +107 -0
  137. package/src/cli/extra-args.test.ts +32 -0
  138. package/src/cli/extra-args.ts +22 -0
  139. package/src/cli/flags.ts +11 -1
  140. package/src/cli/format/auth.ts +92 -0
  141. package/src/cli/format/human.ts +131 -0
  142. package/src/cli/format/index.ts +12 -0
  143. package/src/cli/format/model.ts +29 -0
  144. package/src/cli/format/output.ts +51 -0
  145. package/src/cli/format/project.ts +32 -0
  146. package/src/cli/format/terminal.ts +87 -0
  147. package/src/cli/{thread-format.test.ts → format/thread.test.ts} +6 -4
  148. package/src/cli/format/thread.ts +185 -0
  149. package/src/cli/index.ts +20 -0
  150. package/src/cli/interaction/confirm.ts +64 -0
  151. package/src/cli/{self-action.ts → interaction/self-action.ts} +8 -6
  152. package/src/cli/message-input.ts +1 -1
  153. package/src/cli/model.ts +9 -5
  154. package/src/cli/project.ts +13 -7
  155. package/src/cli/projects/delete.ts +13 -12
  156. package/src/cli/require.ts +9 -7
  157. package/src/cli/runtime/index.ts +1 -0
  158. package/src/cli/runtime/service.ts +28 -0
  159. package/src/{scope → cli/scope}/resolve.ts +11 -9
  160. package/src/cli/terminal/attach.ts +3 -6
  161. package/src/cli/terminal/create.ts +10 -9
  162. package/src/cli/terminal/destroy.ts +13 -11
  163. package/src/cli/terminal/list.ts +10 -9
  164. package/src/cli/terminal/read.test-utils.ts +48 -0
  165. package/src/cli/terminal/read.test.ts +35 -0
  166. package/src/cli/terminal/read.ts +3 -6
  167. package/src/cli/terminal/scope.ts +10 -5
  168. package/src/cli/terminal/shared.ts +1 -1
  169. package/src/cli/terminal/stream.ts +3 -6
  170. package/src/cli/terminal/wait.test.ts +5 -3
  171. package/src/cli/terminal/wait.ts +10 -9
  172. package/src/cli/terminal/write.test.ts +34 -0
  173. package/src/cli/terminal/write.ts +10 -9
  174. package/src/cli/threads/approve.ts +11 -7
  175. package/src/cli/threads/archive.ts +14 -9
  176. package/src/cli/threads/callback.ts +6 -4
  177. package/src/cli/threads/delete.ts +17 -11
  178. package/src/cli/threads/interrupt.ts +14 -9
  179. package/src/cli/threads/list.test.ts +5 -3
  180. package/src/cli/threads/list.ts +10 -10
  181. package/src/cli/threads/messages.ts +11 -7
  182. package/src/cli/threads/respond.ts +11 -7
  183. package/src/cli/threads/send.ts +19 -9
  184. package/src/cli/threads/show.ts +11 -7
  185. package/src/cli/threads/start.ts +18 -13
  186. package/src/cli/threads/unarchive.ts +11 -7
  187. package/src/cli/threads/update.ts +14 -9
  188. package/src/cli/threads/wait.ts +11 -7
  189. package/src/cli/wait-events.ts +1 -1
  190. package/src/config/config.ts +260 -0
  191. package/src/config/credential/cipher-node.ts +40 -0
  192. package/src/config/credential/cipher-web.ts +82 -0
  193. package/src/config/credential/cipher.ts +39 -0
  194. package/src/config/credential/env.ts +13 -0
  195. package/src/config/credential/error.ts +9 -0
  196. package/src/config/credential/index.ts +6 -0
  197. package/src/config/credential/service.test-utils.ts +25 -0
  198. package/src/config/credential/service.test.ts +116 -0
  199. package/src/config/credential/service.ts +205 -0
  200. package/src/config/env/env.test-utils.ts +14 -0
  201. package/src/config/env/env.ts +81 -0
  202. package/src/config/env/index.ts +2 -0
  203. package/src/{layout/base-dir.ts → config/env/layout.ts} +0 -9
  204. package/src/config/environment-name/index.ts +7 -0
  205. package/src/config/environment-name/name.test.ts +66 -0
  206. package/src/config/environment-name/name.ts +49 -0
  207. package/src/config/error.ts +6 -8
  208. package/src/config/index.ts +16 -5
  209. package/src/config/keystore/error.ts +9 -0
  210. package/src/config/keystore/file.ts +96 -0
  211. package/src/config/keystore/index.ts +4 -0
  212. package/src/config/keystore/keyring-node.test.ts +12 -0
  213. package/src/config/keystore/keyring-node.ts +100 -0
  214. package/src/config/keystore/service.test-utils.ts +15 -0
  215. package/src/config/keystore/service.test.ts +18 -0
  216. package/src/config/keystore/service.ts +29 -0
  217. package/src/config/layer.test-utils.ts +45 -0
  218. package/src/config/layer.test.ts +209 -0
  219. package/src/config/paths/index.ts +1 -0
  220. package/src/config/paths/paths.ts +36 -0
  221. package/src/config/persist/file-mode.ts +21 -0
  222. package/src/config/persist/migration.test.ts +71 -0
  223. package/src/config/persist/migration.ts +82 -0
  224. package/src/config/persist/persist.test.ts +113 -0
  225. package/src/config/persist/persist.ts +65 -0
  226. package/src/config/persist/schema.ts +37 -0
  227. package/src/config/platform.test-utils.ts +20 -0
  228. package/src/config/resolve/resolve.test-utils.ts +20 -0
  229. package/src/config/resolve/resolve.test.ts +137 -0
  230. package/src/config/resolve/resolve.ts +104 -0
  231. package/src/config/selection/index.ts +1 -0
  232. package/src/config/selection/resolve.ts +14 -0
  233. package/src/config/selection/service.ts +29 -0
  234. package/src/config/temp-home.test-utils.ts +7 -0
  235. package/src/config/types.ts +49 -0
  236. package/src/config/url/error.ts +8 -0
  237. package/src/config/url/index.ts +7 -0
  238. package/src/config/url/url.test.ts +31 -0
  239. package/src/connection/index.ts +0 -1
  240. package/src/contracts/index.ts +1 -1
  241. package/src/domain/error.ts +20 -1
  242. package/src/domain/helpers.test.ts +1 -1
  243. package/src/domain/helpers.ts +1 -1
  244. package/src/domain/model-config.ts +1 -1
  245. package/src/domain/thread-activities.test.ts +1 -1
  246. package/src/domain/thread-activities.ts +1 -1
  247. package/src/domain/thread-lifecycle.test.ts +1 -1
  248. package/src/domain/thread-lifecycle.ts +1 -1
  249. package/src/effect.test-utils.ts +25 -0
  250. package/src/index.ts +1 -1
  251. package/src/{connection/node.ts → node/connection.ts} +1 -1
  252. package/src/node/index.ts +1 -2
  253. package/src/orchestration/index.ts +0 -1
  254. package/src/orchestration/layer.ts +1 -1
  255. package/src/orchestration/service.ts +1 -1
  256. package/src/rpc/error.ts +2 -2
  257. package/src/rpc/index.ts +2 -3
  258. package/src/rpc/layer.ts +1 -1
  259. package/src/rpc/ws-group.ts +1 -1
  260. package/src/runtime/index.ts +2 -1
  261. package/src/runtime/layer.test-utils.ts +32 -0
  262. package/src/runtime/layer.test.ts +66 -0
  263. package/src/runtime/layer.ts +15 -9
  264. package/src/t3tools/index.ts +1 -1
  265. package/src/types/marked-terminal.d.ts +11 -0
  266. package/dist/layout.js +0 -2
  267. package/dist/scope.js +0 -2
  268. package/dist/src/cli/output-format.d.ts +0 -11
  269. package/dist/src/config/layer.d.ts +0 -22
  270. package/dist/src/config/service.d.ts +0 -22
  271. package/dist/src/environment/layer.d.ts +0 -3
  272. package/dist/src/environment/service.d.ts +0 -12
  273. package/dist/src/layout/index.d.ts +0 -1
  274. package/dist/upstream-t3code/packages/contracts/src/auth.d.ts +0 -441
  275. package/dist/upstream-t3code/packages/contracts/src/baseSchemas.d.ts +0 -38
  276. package/dist/upstream-t3code/packages/contracts/src/desktopBootstrap.d.ts +0 -14
  277. package/dist/upstream-t3code/packages/contracts/src/editor.d.ts +0 -124
  278. package/dist/upstream-t3code/packages/contracts/src/environment.d.ts +0 -64
  279. package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +0 -762
  280. package/dist/upstream-t3code/packages/contracts/src/filesystem.d.ts +0 -26
  281. package/dist/upstream-t3code/packages/contracts/src/git.d.ts +0 -500
  282. package/dist/upstream-t3code/packages/contracts/src/index.d.ts +0 -25
  283. package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +0 -490
  284. package/dist/upstream-t3code/packages/contracts/src/keybindings.d.ts +0 -133
  285. package/dist/upstream-t3code/packages/contracts/src/model.d.ts +0 -112
  286. package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +0 -6682
  287. package/dist/upstream-t3code/packages/contracts/src/project.d.ts +0 -45
  288. package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +0 -116
  289. package/dist/upstream-t3code/packages/contracts/src/providerInstance.d.ts +0 -99
  290. package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +0 -4276
  291. package/dist/upstream-t3code/packages/contracts/src/relay.d.ts +0 -1262
  292. package/dist/upstream-t3code/packages/contracts/src/relayClient.d.ts +0 -48
  293. package/dist/upstream-t3code/packages/contracts/src/remoteAccess.d.ts +0 -45
  294. package/dist/upstream-t3code/packages/contracts/src/review.d.ts +0 -37
  295. package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +0 -8218
  296. package/dist/upstream-t3code/packages/contracts/src/server.d.ts +0 -2291
  297. package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +0 -271
  298. package/dist/upstream-t3code/packages/contracts/src/sourceControl.d.ts +0 -177
  299. package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +0 -330
  300. package/dist/upstream-t3code/packages/contracts/src/vcs.d.ts +0 -148
  301. package/src/cli/auth-format.ts +0 -45
  302. package/src/cli/confirm.ts +0 -31
  303. package/src/cli/model-format.ts +0 -18
  304. package/src/cli/output-format.ts +0 -45
  305. package/src/cli/project-format.ts +0 -18
  306. package/src/cli/terminal/commands.test.ts +0 -101
  307. package/src/cli/terminal-format.ts +0 -167
  308. package/src/cli/thread-format.ts +0 -128
  309. package/src/config/layer.ts +0 -105
  310. package/src/config/service.ts +0 -26
  311. package/src/environment/layer.ts +0 -12
  312. package/src/environment/service.ts +0 -13
  313. package/src/layout/index.ts +0 -1
  314. /package/dist/src/{scope → cli/scope}/index.d.ts +0 -0
  315. /package/dist/src/config/{url.d.ts → url/url.d.ts} +0 -0
  316. /package/src/{scope → cli/scope}/index.ts +0 -0
  317. /package/src/config/{url.ts → url/url.ts} +0 -0
@@ -0,0 +1,100 @@
1
+ import * as Cause from "effect/Cause";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Encoding from "effect/Encoding";
4
+ import * as Layer from "effect/Layer";
5
+ import * as Predicate from "effect/Predicate";
6
+ import * as Result from "effect/Result";
7
+
8
+ import { KeystoreUnavailableError } from "./error.ts";
9
+ import {
10
+ masterKeyByteLength,
11
+ T3MasterKeyKeystoreFactory,
12
+ type MasterKeyKeystore,
13
+ type MasterKeyReadResult,
14
+ } from "./service.ts";
15
+
16
+ type KeyringModule = typeof import("@napi-rs/keyring");
17
+
18
+ const keyringService = "t3cli";
19
+ const keyringAccount = "master-key";
20
+
21
+ const loadKeyringModuleOnce = Effect.tryPromise(
22
+ () => import("@napi-rs/keyring") as Promise<KeyringModule>,
23
+ );
24
+
25
+ const loadKeyringModuleMemoized = Effect.cached(loadKeyringModuleOnce);
26
+
27
+ const loadKeyringModule = Effect.flatMap(loadKeyringModuleMemoized, (load) =>
28
+ load.pipe(
29
+ Effect.mapError(
30
+ (error) =>
31
+ new KeystoreUnavailableError({
32
+ reason:
33
+ Predicate.hasProperty(error.cause, "code") &&
34
+ error.cause.code === "ERR_MODULE_NOT_FOUND"
35
+ ? "module-not-found"
36
+ : "backend-unavailable",
37
+ cause: error,
38
+ }),
39
+ ),
40
+ ),
41
+ );
42
+
43
+ export function parseKeyringPassword(password: string | null): MasterKeyReadResult {
44
+ if (password === null || password.length === 0) {
45
+ return { kind: "missing" };
46
+ }
47
+ return Result.match(Encoding.decodeBase64(password.trim()), {
48
+ onFailure: () => ({
49
+ kind: "corrupt" as const,
50
+ message: "invalid base64 key",
51
+ }),
52
+ onSuccess: (key) => {
53
+ if (key.byteLength !== masterKeyByteLength) {
54
+ return {
55
+ kind: "corrupt" as const,
56
+ message: "unexpected key length",
57
+ };
58
+ }
59
+ return { kind: "present" as const, key };
60
+ },
61
+ });
62
+ }
63
+
64
+ function createKeyringKeystore(keyring: KeyringModule): MasterKeyKeystore {
65
+ const entry = new keyring.Entry(keyringService, keyringAccount);
66
+ return {
67
+ read: () =>
68
+ Effect.sync(() => {
69
+ try {
70
+ return parseKeyringPassword(entry.getPassword());
71
+ } catch {
72
+ return {
73
+ kind: "unavailable" as const,
74
+ message: "failed to read credential key from OS keyring",
75
+ };
76
+ }
77
+ }),
78
+ write: (key) =>
79
+ Effect.try({
80
+ try: () => {
81
+ entry.setPassword(Encoding.encodeBase64(key));
82
+ },
83
+ catch: (cause) =>
84
+ new KeystoreUnavailableError({
85
+ reason: "backend-unavailable",
86
+ cause: new Cause.UnknownError(cause),
87
+ }),
88
+ }),
89
+ };
90
+ }
91
+
92
+ const make = (): T3MasterKeyKeystoreFactory["Service"] => ({
93
+ make: () =>
94
+ Effect.gen(function* () {
95
+ const keyring = yield* loadKeyringModule;
96
+ return createKeyringKeystore(keyring);
97
+ }),
98
+ });
99
+
100
+ export const layerNode = Layer.succeed(T3MasterKeyKeystoreFactory, make());
@@ -0,0 +1,15 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+
4
+ import { KeystoreUnavailableError } from "./error.ts";
5
+ import { T3MasterKeyKeystoreFactory } from "./service.ts";
6
+
7
+ export const unavailableKeystoreFactoryLayer = Layer.succeed(T3MasterKeyKeystoreFactory)({
8
+ make: () =>
9
+ Effect.fail(
10
+ new KeystoreUnavailableError({
11
+ reason: "module-not-found",
12
+ cause: new Error("keyring unavailable in test"),
13
+ }),
14
+ ),
15
+ });
@@ -0,0 +1,18 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import { describe, it } from "@effect/vitest";
4
+ import { expect } from "vite-plus/test";
5
+
6
+ import { shouldUseFileKeystoreForRead } from "./service.ts";
7
+
8
+ describe("shouldUseFileKeystoreForRead", () => {
9
+ it("treats unavailable keyring reads as file-keystore fallback", () => {
10
+ const result = { kind: "unavailable" as const, message: "failed" };
11
+ expect(shouldUseFileKeystoreForRead(result)).toBe(true);
12
+ });
13
+
14
+ it("does not fall back for corrupt keyring reads", () => {
15
+ const result = { kind: "corrupt" as const, message: "invalid key" };
16
+ expect(shouldUseFileKeystoreForRead(result)).toBe(false);
17
+ });
18
+ });
@@ -0,0 +1,29 @@
1
+ import * as Context from "effect/Context";
2
+ import type * as Effect from "effect/Effect";
3
+
4
+ import type { ConfigError } from "../error.ts";
5
+ import type { KeystoreUnavailableError } from "./error.ts";
6
+
7
+ export const masterKeyByteLength = 32;
8
+
9
+ export type MasterKeyReadResult =
10
+ | { readonly kind: "missing" }
11
+ | { readonly kind: "present"; readonly key: Uint8Array }
12
+ | { readonly kind: "corrupt"; readonly message: string }
13
+ | { readonly kind: "unavailable"; readonly message: string };
14
+
15
+ export type MasterKeyKeystore = {
16
+ readonly read: () => Effect.Effect<MasterKeyReadResult, ConfigError>;
17
+ readonly write: (key: Uint8Array) => Effect.Effect<void, ConfigError | KeystoreUnavailableError>;
18
+ };
19
+
20
+ export class T3MasterKeyKeystoreFactory extends Context.Service<
21
+ T3MasterKeyKeystoreFactory,
22
+ {
23
+ readonly make: () => Effect.Effect<MasterKeyKeystore, KeystoreUnavailableError>;
24
+ }
25
+ >()("t3cli/T3MasterKeyKeystoreFactory") {}
26
+
27
+ export function shouldUseFileKeystoreForRead(result: MasterKeyReadResult) {
28
+ return result.kind === "missing" || result.kind === "unavailable";
29
+ }
@@ -0,0 +1,45 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+
4
+ import * as Credential from "./credential/service.ts";
5
+ import * as Config from "./config.ts";
6
+ import * as Selection from "./selection/service.ts";
7
+ import { CliRuntime } from "../cli/runtime/service.ts";
8
+ import { ConfigPlatformLayer } from "./platform.test-utils.ts";
9
+ import { t3CliEnvConfigLayer } from "./env/env.test-utils.ts";
10
+ import * as CredentialCipherWeb from "./credential/cipher-web.ts";
11
+ import * as Keystore from "./keystore/service.test-utils.ts";
12
+
13
+ export type T3ConfigLayerTestInput = {
14
+ readonly selection?: string;
15
+ readonly env?: Readonly<Record<string, string | undefined>>;
16
+ readonly useSelectionLive?: boolean;
17
+ };
18
+
19
+ export function t3ConfigDepsLayer(homeDir: string, input: T3ConfigLayerTestInput = {}) {
20
+ const cliRuntimeLayer = CliRuntime.layerTest({ cwd: homeDir });
21
+ const configLayer = t3CliEnvConfigLayer(homeDir, input.env ?? {});
22
+ const platformEnvLayer = Layer.mergeAll(cliRuntimeLayer, configLayer);
23
+ const selectionLayer =
24
+ input.useSelectionLive === true
25
+ ? Selection.layer.pipe(Layer.provide(platformEnvLayer))
26
+ : Layer.succeed(Selection.T3ConfigSelection)({
27
+ getSelectedEnvironment: () => Effect.succeed(input.selection),
28
+ });
29
+
30
+ return Config.layer.pipe(
31
+ Layer.provide(Credential.layer),
32
+ Layer.provide(
33
+ Layer.mergeAll(
34
+ platformEnvLayer,
35
+ selectionLayer,
36
+ CredentialCipherWeb.layerWeb,
37
+ Keystore.unavailableKeystoreFactoryLayer,
38
+ ),
39
+ ),
40
+ );
41
+ }
42
+
43
+ export function t3ConfigLayerTest(homeDir: string, input: T3ConfigLayerTestInput = {}) {
44
+ return Layer.mergeAll(ConfigPlatformLayer, t3ConfigDepsLayer(homeDir, input));
45
+ }
@@ -0,0 +1,209 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import * as Effect from "effect/Effect";
4
+ import { assert, describe, it } from "@effect/vitest";
5
+
6
+ import { T3Config } from "./config.ts";
7
+ import { ConfigPlatformLayer } from "./platform.test-utils.ts";
8
+ import { makeTempHomeScoped } from "./temp-home.test-utils.ts";
9
+ import { t3ConfigDepsLayer } from "./layer.test-utils.ts";
10
+
11
+ describe("T3Config", () => {
12
+ it.effect(
13
+ "keeps default unchanged when upserting an existing environment without makeDefault",
14
+ () =>
15
+ Effect.gen(function* () {
16
+ return yield* makeTempHomeScoped("t3cli-config-");
17
+ }).pipe(
18
+ Effect.flatMap((homeDir) =>
19
+ Effect.gen(function* () {
20
+ const config = yield* T3Config;
21
+ yield* config.upsertEnvironment({
22
+ name: "home",
23
+ url: "https://home.example",
24
+ token: "home-token",
25
+ local: false,
26
+ });
27
+ yield* config.upsertEnvironment({
28
+ name: "work",
29
+ url: "https://work.example",
30
+ token: "work-token",
31
+ local: false,
32
+ });
33
+ yield* config.upsertEnvironment({
34
+ name: "work",
35
+ url: "https://work.example",
36
+ token: "work-token-2",
37
+ local: false,
38
+ });
39
+ const listed = yield* config.listEnvironments();
40
+ assert.equal(listed.find((environment) => environment.name === "home")?.default, true);
41
+ assert.equal(listed.find((environment) => environment.name === "work")?.default, false);
42
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir))),
43
+ ),
44
+ Effect.provide(ConfigPlatformLayer),
45
+ Effect.scoped,
46
+ ),
47
+ );
48
+
49
+ it.effect("promotes default when upserting with makeDefault", () =>
50
+ Effect.gen(function* () {
51
+ return yield* makeTempHomeScoped("t3cli-config-");
52
+ }).pipe(
53
+ Effect.flatMap((homeDir) =>
54
+ Effect.gen(function* () {
55
+ const config = yield* T3Config;
56
+ yield* config.upsertEnvironment({
57
+ name: "home",
58
+ url: "https://home.example",
59
+ token: "home-token",
60
+ local: false,
61
+ });
62
+ yield* config.upsertEnvironment({
63
+ name: "work",
64
+ url: "https://work.example",
65
+ token: "work-token",
66
+ local: false,
67
+ });
68
+ yield* config.upsertEnvironment({
69
+ name: "work",
70
+ url: "https://work.example",
71
+ token: "work-token-2",
72
+ local: false,
73
+ makeDefault: true,
74
+ });
75
+ const listed = yield* config.listEnvironments();
76
+ assert.equal(listed.find((environment) => environment.name === "home")?.default, false);
77
+ assert.equal(listed.find((environment) => environment.name === "work")?.default, true);
78
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir))),
79
+ ),
80
+ Effect.provide(ConfigPlatformLayer),
81
+ Effect.scoped,
82
+ ),
83
+ );
84
+
85
+ it.effect("clears default when removing the default environment", () =>
86
+ Effect.gen(function* () {
87
+ return yield* makeTempHomeScoped("t3cli-config-");
88
+ }).pipe(
89
+ Effect.flatMap((homeDir) =>
90
+ Effect.gen(function* () {
91
+ const config = yield* T3Config;
92
+ yield* config.upsertEnvironment({
93
+ name: "home",
94
+ url: "https://home.example",
95
+ token: "home-token",
96
+ local: false,
97
+ });
98
+ yield* config.upsertEnvironment({
99
+ name: "work",
100
+ url: "https://work.example",
101
+ token: "work-token",
102
+ local: false,
103
+ });
104
+ yield* config.removeEnvironment("home");
105
+ const defaultName = yield* config.getDefaultEnvironmentName();
106
+ assert.equal(defaultName, undefined);
107
+ const listed = yield* config.listEnvironments();
108
+ assert.equal(
109
+ listed.every((environment) => !environment.default),
110
+ true,
111
+ );
112
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir))),
113
+ ),
114
+ Effect.provide(ConfigPlatformLayer),
115
+ Effect.scoped,
116
+ ),
117
+ );
118
+
119
+ it.effect("resolves selected environment from config selection service", () =>
120
+ Effect.gen(function* () {
121
+ return yield* makeTempHomeScoped("t3cli-config-");
122
+ }).pipe(
123
+ Effect.flatMap((homeDir) =>
124
+ Effect.gen(function* () {
125
+ const config = yield* T3Config;
126
+ yield* config.upsertEnvironment({
127
+ name: "home",
128
+ url: "https://home.example",
129
+ token: "home-token",
130
+ local: false,
131
+ });
132
+ yield* config.upsertEnvironment({
133
+ name: "work",
134
+ url: "https://work.example",
135
+ token: "work-token",
136
+ local: true,
137
+ });
138
+ const resolved = yield* config.resolve();
139
+ assert.equal(resolved.source, "config");
140
+ if (resolved.source === "config") {
141
+ assert.equal(resolved.environment, "work");
142
+ assert.equal(resolved.token, "work-token");
143
+ }
144
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir, { selection: "work" }))),
145
+ ),
146
+ Effect.provide(ConfigPlatformLayer),
147
+ Effect.scoped,
148
+ ),
149
+ );
150
+
151
+ it.effect(
152
+ "resolves env override with local=false even when selected stored environment is local",
153
+ () =>
154
+ Effect.gen(function* () {
155
+ return yield* makeTempHomeScoped("t3cli-config-");
156
+ }).pipe(
157
+ Effect.flatMap((homeDir) =>
158
+ Effect.gen(function* () {
159
+ const config = yield* T3Config;
160
+ yield* config.upsertEnvironment({
161
+ name: "work",
162
+ url: "http://localhost:8787",
163
+ token: "work-token",
164
+ local: true,
165
+ });
166
+ const resolved = yield* config.resolve();
167
+ assert.equal(resolved.source, "env");
168
+ assert.equal(resolved.local, false);
169
+ assert.equal(resolved.url, "https://remote.example");
170
+ assert.equal(resolved.token, "env-token");
171
+ }).pipe(
172
+ Effect.provide(
173
+ t3ConfigDepsLayer(homeDir, {
174
+ selection: "work",
175
+ env: {
176
+ T3CODE_URL: "https://remote.example",
177
+ T3CODE_TOKEN: "env-token",
178
+ },
179
+ }),
180
+ ),
181
+ ),
182
+ ),
183
+ Effect.provide(ConfigPlatformLayer),
184
+ Effect.scoped,
185
+ ),
186
+ );
187
+
188
+ it.effect("reads default environment name without decrypting tokens", () =>
189
+ Effect.gen(function* () {
190
+ return yield* makeTempHomeScoped("t3cli-config-");
191
+ }).pipe(
192
+ Effect.flatMap((homeDir) =>
193
+ Effect.gen(function* () {
194
+ const config = yield* T3Config;
195
+ yield* config.upsertEnvironment({
196
+ name: "home",
197
+ url: "https://home.example",
198
+ token: "home-token",
199
+ local: false,
200
+ });
201
+ const defaultName = yield* config.getDefaultEnvironmentName();
202
+ assert.equal(defaultName, "home");
203
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir))),
204
+ ),
205
+ Effect.provide(ConfigPlatformLayer),
206
+ Effect.scoped,
207
+ ),
208
+ );
209
+ });
@@ -0,0 +1 @@
1
+ export { resolveConfigFilePath, resolveKeyFilePath, resolveT3cliConfigDir } from "./paths.ts";
@@ -0,0 +1,36 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Option from "effect/Option";
3
+ import * as Path from "effect/Path";
4
+
5
+ import { loadT3CliEnv } from "../env/env.ts";
6
+ import { ConfigError } from "../error.ts";
7
+
8
+ export const resolveT3cliConfigDir = Effect.fn("resolveT3cliConfigDir")(function* () {
9
+ const path = yield* Path.Path;
10
+ const t3CliEnv = yield* loadT3CliEnv.pipe(
11
+ Effect.mapError(
12
+ (error) => new ConfigError({ message: "failed to load CLI environment", cause: error }),
13
+ ),
14
+ );
15
+ const home = yield* Option.match(t3CliEnv.home, {
16
+ onNone: () => Effect.die("HOME is not set"),
17
+ onSome: Effect.succeed,
18
+ });
19
+ const root = Option.match(t3CliEnv.xdgConfigHome, {
20
+ onNone: () => path.join(home, ".config"),
21
+ onSome: (xdgConfigHome) => xdgConfigHome,
22
+ });
23
+ return path.join(root, "t3cli");
24
+ });
25
+
26
+ export const resolveConfigFilePath = Effect.fn("resolveConfigFilePath")(function* () {
27
+ const path = yield* Path.Path;
28
+ const configDir = yield* resolveT3cliConfigDir();
29
+ return path.join(configDir, "config.json");
30
+ });
31
+
32
+ export const resolveKeyFilePath = Effect.fn("resolveKeyFilePath")(function* () {
33
+ const path = yield* Path.Path;
34
+ const configDir = yield* resolveT3cliConfigDir();
35
+ return path.join(configDir, "key");
36
+ });
@@ -0,0 +1,21 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+
4
+ import { ConfigError } from "../error.ts";
5
+
6
+ const privateFileMode = 0o600;
7
+
8
+ export const hardenPrivateFileMode = Effect.fn("hardenPrivateFileMode")(function* (
9
+ filePath: string,
10
+ label: "config" | "credential key",
11
+ ) {
12
+ const fs = yield* FileSystem.FileSystem;
13
+ yield* fs
14
+ .chmod(filePath, privateFileMode)
15
+ .pipe(
16
+ Effect.mapError(
17
+ (error) =>
18
+ new ConfigError({ message: `failed to set ${label} file permissions`, cause: error }),
19
+ ),
20
+ );
21
+ });
@@ -0,0 +1,71 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import * as Cause from "effect/Cause";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Exit from "effect/Exit";
6
+ import * as Option from "effect/Option";
7
+ import { assert, describe, it } from "@effect/vitest";
8
+
9
+ import { ConfigError } from "../error.ts";
10
+ import { T3CredentialCrypto } from "../credential/service.ts";
11
+ import { ConfigPlatformLayer } from "../platform.test-utils.ts";
12
+ import { makeTempHomeScoped } from "../temp-home.test-utils.ts";
13
+ import { t3CredentialCryptoDepsLayer } from "../credential/service.test-utils.ts";
14
+ import { migrateV1FileToEncrypted } from "./migration.ts";
15
+
16
+ describe("migrateV1FileToEncrypted", () => {
17
+ it.effect("migrates v1 flat config and roundtrips encrypted tokens", () =>
18
+ Effect.gen(function* () {
19
+ return yield* makeTempHomeScoped("t3cli-migration-");
20
+ }).pipe(
21
+ Effect.flatMap((homeDir) =>
22
+ Effect.gen(function* () {
23
+ const migrated = yield* migrateV1FileToEncrypted({
24
+ url: "https://app.example.com",
25
+ token: "secret-token",
26
+ local: false,
27
+ });
28
+ assert.equal(migrated.default, "app.example.com");
29
+ const crypto = yield* T3CredentialCrypto;
30
+ const token = yield* crypto.decrypt({
31
+ environmentName: "app.example.com",
32
+ url: "https://app.example.com",
33
+ local: false,
34
+ token: migrated.environments["app.example.com"]!.token,
35
+ });
36
+ assert.equal(token, "secret-token");
37
+ }).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir))),
38
+ ),
39
+ Effect.provide(ConfigPlatformLayer),
40
+ Effect.scoped,
41
+ ),
42
+ );
43
+
44
+ it.effect("maps invalid v1 urls to ConfigError when migrating directly", () =>
45
+ Effect.gen(function* () {
46
+ return yield* makeTempHomeScoped("t3cli-migrate-url-error-");
47
+ }).pipe(
48
+ Effect.flatMap((homeDir) =>
49
+ Effect.gen(function* () {
50
+ const exit = yield* migrateV1FileToEncrypted({
51
+ url: "not-a-url",
52
+ token: "secret-token",
53
+ local: false,
54
+ }).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)), Effect.exit);
55
+ assert.isTrue(Exit.isFailure(exit));
56
+ if (!Exit.isFailure(exit)) {
57
+ return;
58
+ }
59
+ const error = Cause.findErrorOption(exit.cause);
60
+ assert.isTrue(Option.isSome(error));
61
+ if (Option.isSome(error)) {
62
+ assert.instanceOf(error.value, ConfigError);
63
+ assert.equal(error.value.message, "invalid url");
64
+ }
65
+ }),
66
+ ),
67
+ Effect.provide(ConfigPlatformLayer),
68
+ Effect.scoped,
69
+ ),
70
+ );
71
+ });
@@ -0,0 +1,82 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Option from "effect/Option";
3
+ import * as Schema from "effect/Schema";
4
+
5
+ import { T3CredentialCrypto } from "../credential/service.ts";
6
+ import { migrateV1EnvironmentName, validateEnvironmentName } from "../environment-name/name.ts";
7
+ import { ConfigError } from "../error.ts";
8
+ import {
9
+ StoredConfigV1FileSchema,
10
+ StoredConfigV2FileSchema,
11
+ type StoredConfigV1File,
12
+ } from "./schema.ts";
13
+ import type { EncryptedConfig } from "../types.ts";
14
+ import { normalizeHttpBaseUrl } from "../url/url.ts";
15
+
16
+ export const emptyEncryptedConfig = (): EncryptedConfig => ({
17
+ version: 2,
18
+ environments: {},
19
+ });
20
+
21
+ export const readEncryptedConfigFromValue = Effect.fn("readEncryptedConfigFromValue")(function* (
22
+ value: unknown,
23
+ ) {
24
+ return yield* Effect.gen(function* () {
25
+ const v2 = yield* Schema.decodeUnknownEffect(StoredConfigV2FileSchema)(value).pipe(
26
+ Effect.option,
27
+ );
28
+ if (Option.isSome(v2)) {
29
+ return { config: v2.value, migratedFromV1: false as const };
30
+ }
31
+ const v1 = yield* Schema.decodeUnknownEffect(StoredConfigV1FileSchema)(value);
32
+ const migrated = yield* migrateV1FileToEncrypted(v1);
33
+ return { config: migrated, migratedFromV1: true as const };
34
+ }).pipe(
35
+ Effect.catchTags({
36
+ SchemaError: (error) =>
37
+ Effect.fail(new ConfigError({ message: "failed to read config", cause: error })),
38
+ }),
39
+ );
40
+ });
41
+
42
+ export const migrateV1FileToEncrypted = Effect.fn("migrateV1FileToEncrypted")(function* (
43
+ config: StoredConfigV1File,
44
+ ) {
45
+ if (
46
+ (config.url === undefined || config.url.length === 0) &&
47
+ (config.token === undefined || config.token.length === 0)
48
+ ) {
49
+ return emptyEncryptedConfig();
50
+ }
51
+ if (config.url === undefined || config.token === undefined) {
52
+ return yield* Effect.fail(
53
+ new ConfigError({ message: "failed to read config: incomplete v1 credentials" }),
54
+ );
55
+ }
56
+ const name = yield* migrateV1EnvironmentName(config).pipe(
57
+ Effect.flatMap((migratedName) =>
58
+ validateEnvironmentName(migratedName).pipe(Effect.as(migratedName)),
59
+ ),
60
+ );
61
+ const normalizedUrl = yield* normalizeHttpBaseUrl(config.url).pipe(
62
+ Effect.mapError((error) => new ConfigError({ message: error.message, cause: error.cause })),
63
+ );
64
+ const crypto = yield* T3CredentialCrypto;
65
+ const token = yield* crypto.encrypt({
66
+ environmentName: name,
67
+ url: normalizedUrl,
68
+ local: config.local ?? false,
69
+ token: config.token,
70
+ });
71
+ return {
72
+ version: 2 as const,
73
+ default: name,
74
+ environments: {
75
+ [name]: {
76
+ url: normalizedUrl,
77
+ local: config.local ?? false,
78
+ token,
79
+ },
80
+ },
81
+ } satisfies EncryptedConfig;
82
+ });