t3code-cli 0.10.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 (328) hide show
  1. package/README.md +75 -11
  2. package/dist/application.js +2 -2
  3. package/dist/auth.js +1 -1
  4. package/dist/bin.js +86754 -2675
  5. package/dist/cli.js +15 -2
  6. package/dist/config.js +120 -2
  7. package/dist/connection.js +2 -12
  8. package/dist/index.js +1 -1
  9. package/dist/node.js +6 -0
  10. package/dist/orchestration.js +2 -2
  11. package/dist/rpc.js +2 -2
  12. package/dist/runtime.js +2 -2
  13. package/dist/shared.js +43850 -38040
  14. package/dist/src/application/actions.d.ts +25 -0
  15. package/dist/src/application/index.d.ts +3 -2
  16. package/dist/src/application/layer.d.ts +99 -842
  17. package/dist/src/application/model-selection.d.ts +17 -2
  18. package/dist/src/application/models.d.ts +1 -87
  19. package/dist/src/application/project-commands.d.ts +31 -2
  20. package/dist/src/application/projects.d.ts +15 -137
  21. package/dist/src/application/service.d.ts +147 -5
  22. package/dist/src/application/shell-sequence.d.ts +5 -4
  23. package/dist/src/application/terminals.d.ts +66 -0
  24. package/dist/src/application/thread-commands.d.ts +48 -1
  25. package/dist/src/application/thread-update.d.ts +8 -0
  26. package/dist/src/application/thread-wait.d.ts +4 -6
  27. package/dist/src/application/threads.d.ts +36 -14
  28. package/dist/src/auth/error.d.ts +2 -1
  29. package/dist/src/auth/index.d.ts +1 -1
  30. package/dist/src/auth/layer.d.ts +34 -6
  31. package/dist/src/auth/local-base-dir.d.ts +5 -2
  32. package/dist/src/auth/local-origin.d.ts +3 -3
  33. package/dist/src/auth/local-token.d.ts +3 -3
  34. package/dist/src/auth/service.d.ts +15 -3
  35. package/dist/src/auth/type.d.ts +40 -1
  36. package/dist/src/cli/env/flag.d.ts +2 -0
  37. package/dist/src/cli/env/index.d.ts +2 -0
  38. package/dist/src/cli/env/selection-layer.d.ts +3 -0
  39. package/dist/src/cli/flags.d.ts +5 -0
  40. package/dist/src/cli/format/index.d.ts +1 -0
  41. package/dist/src/cli/format/output.d.ts +12 -0
  42. package/dist/src/cli/index.d.ts +4 -0
  43. package/dist/src/cli/runtime/index.d.ts +1 -0
  44. package/dist/src/cli/runtime/service.d.ts +16 -0
  45. package/dist/src/{scope → cli/scope}/resolve.d.ts +5 -4
  46. package/dist/src/config/config.d.ts +49 -0
  47. package/dist/src/config/credential/cipher-node.d.ts +3 -0
  48. package/dist/src/config/credential/cipher-web.d.ts +3 -0
  49. package/dist/src/config/credential/cipher.d.ts +29 -0
  50. package/dist/src/config/credential/env.d.ts +3 -0
  51. package/dist/src/config/credential/error.d.ts +8 -0
  52. package/dist/src/config/credential/index.d.ts +6 -0
  53. package/dist/src/config/credential/service.d.ts +34 -0
  54. package/dist/src/config/env/env.d.ts +55 -0
  55. package/dist/src/config/env/index.d.ts +2 -0
  56. package/dist/src/{layout/base-dir.d.ts → config/env/layout.d.ts} +0 -1
  57. package/dist/src/config/environment-name/index.d.ts +1 -0
  58. package/dist/src/config/environment-name/name.d.ts +10 -0
  59. package/dist/src/config/error.d.ts +4 -11
  60. package/dist/src/config/index.d.ts +10 -5
  61. package/dist/src/config/keystore/error.d.ts +8 -0
  62. package/dist/src/config/keystore/file.d.ts +9 -0
  63. package/dist/src/config/keystore/index.d.ts +4 -0
  64. package/dist/src/config/keystore/keyring-node.d.ts +4 -0
  65. package/dist/src/config/keystore/service.d.ts +33 -0
  66. package/dist/src/config/paths/index.d.ts +1 -0
  67. package/dist/src/config/paths/paths.d.ts +6 -0
  68. package/dist/src/config/persist/file-mode.d.ts +4 -0
  69. package/dist/src/config/persist/migration.d.ts +68 -0
  70. package/dist/src/config/persist/persist.d.ts +40 -0
  71. package/dist/src/config/persist/schema.d.ts +68 -0
  72. package/dist/src/config/resolve/resolve.d.ts +40 -0
  73. package/dist/src/config/selection/index.d.ts +1 -0
  74. package/dist/src/config/selection/resolve.d.ts +4 -0
  75. package/dist/src/config/selection/service.d.ts +10 -0
  76. package/dist/src/config/types.d.ts +36 -0
  77. package/dist/src/config/url/error.d.ts +10 -0
  78. package/dist/src/config/url/index.d.ts +2 -0
  79. package/dist/src/connection/index.d.ts +0 -1
  80. package/dist/src/contracts/index.d.ts +1 -1
  81. package/dist/src/domain/error.d.ts +27 -1
  82. package/dist/src/domain/helpers.d.ts +8 -2
  83. package/dist/src/domain/model-config.d.ts +30 -1
  84. package/dist/src/domain/thread-activities.d.ts +1 -1
  85. package/dist/src/domain/thread-lifecycle.d.ts +3 -2
  86. package/dist/src/index.d.ts +1 -2
  87. package/dist/src/{connection/node.d.ts → node/connection.d.ts} +1 -1
  88. package/dist/src/node/index.d.ts +1 -0
  89. package/dist/src/orchestration/index.d.ts +1 -1
  90. package/dist/src/orchestration/layer.d.ts +120 -829
  91. package/dist/src/orchestration/service.d.ts +2 -1
  92. package/dist/src/rpc/error.d.ts +5 -3
  93. package/dist/src/rpc/index.d.ts +5 -1
  94. package/dist/src/rpc/layer.d.ts +1166 -40
  95. package/dist/src/rpc/operation.d.ts +6280 -0
  96. package/dist/src/rpc/ws-group.d.ts +372 -27
  97. package/dist/src/runtime/index.d.ts +1 -2
  98. package/dist/src/runtime/layer.d.ts +12 -8
  99. package/dist/src/t3tools/index.d.ts +1 -1
  100. package/dist/t3tools.js +2 -2
  101. package/package.json +27 -19
  102. package/src/application/actions.test.ts +328 -0
  103. package/src/application/actions.ts +475 -0
  104. package/src/application/index.ts +32 -2
  105. package/src/application/layer.ts +43 -5
  106. package/src/application/model-selection.ts +70 -4
  107. package/src/application/models.ts +14 -14
  108. package/src/application/project-commands.ts +38 -5
  109. package/src/application/projects.ts +49 -20
  110. package/src/application/service.ts +275 -78
  111. package/src/application/shell-sequence.ts +6 -8
  112. package/src/application/terminals.ts +208 -0
  113. package/src/application/thread-commands.test.ts +43 -0
  114. package/src/application/thread-commands.ts +82 -1
  115. package/src/application/thread-update.ts +72 -0
  116. package/src/application/thread-wait.ts +47 -48
  117. package/src/application/threads.test.ts +172 -0
  118. package/src/application/threads.ts +128 -45
  119. package/src/auth/error.ts +2 -1
  120. package/src/auth/index.ts +7 -0
  121. package/src/auth/layer.test-utils.ts +34 -0
  122. package/src/auth/layer.ts +116 -34
  123. package/src/auth/local-base-dir.ts +13 -6
  124. package/src/auth/local-origin.ts +10 -7
  125. package/src/auth/local-token.ts +28 -23
  126. package/src/auth/service.test.ts +200 -0
  127. package/src/auth/service.ts +27 -3
  128. package/src/auth/transport.ts +2 -2
  129. package/src/auth/type.ts +47 -1
  130. package/src/bin.ts +26 -6
  131. package/src/cli/action-format.ts +110 -0
  132. package/src/cli/action.ts +398 -0
  133. package/src/cli/app.ts +8 -0
  134. package/src/cli/auth.ts +76 -23
  135. package/src/cli/env/flag.ts +8 -0
  136. package/src/cli/env/index.ts +2 -0
  137. package/src/cli/env/selection-layer.test.ts +49 -0
  138. package/src/cli/env/selection-layer.ts +27 -0
  139. package/src/cli/env.ts +107 -0
  140. package/src/cli/error.ts +26 -3
  141. package/src/cli/extra-args.test.ts +32 -0
  142. package/src/cli/extra-args.ts +22 -0
  143. package/src/cli/flags.ts +26 -1
  144. package/src/cli/format/auth.ts +92 -0
  145. package/src/cli/format/human.ts +131 -0
  146. package/src/cli/format/index.ts +12 -0
  147. package/src/cli/format/model.ts +29 -0
  148. package/src/cli/format/output.ts +51 -0
  149. package/src/cli/format/project.ts +32 -0
  150. package/src/cli/format/terminal.ts +87 -0
  151. package/src/cli/format/thread.test.ts +34 -0
  152. package/src/cli/format/thread.ts +185 -0
  153. package/src/cli/index.ts +20 -0
  154. package/src/cli/input/layer.ts +20 -0
  155. package/src/cli/input/service.ts +1 -0
  156. package/src/cli/interaction/confirm.ts +64 -0
  157. package/src/cli/interaction/self-action.ts +36 -0
  158. package/src/cli/message-input.ts +1 -1
  159. package/src/cli/model.ts +9 -5
  160. package/src/cli/project.ts +15 -8
  161. package/src/cli/projects/delete.ts +51 -0
  162. package/src/cli/require.ts +9 -7
  163. package/src/cli/runtime/index.ts +1 -0
  164. package/src/cli/runtime/service.ts +28 -0
  165. package/src/{scope → cli/scope}/resolve.ts +11 -9
  166. package/src/cli/terminal/attach.ts +29 -0
  167. package/src/cli/terminal/create.ts +55 -0
  168. package/src/cli/terminal/destroy.ts +62 -0
  169. package/src/cli/terminal/encoding.test.ts +63 -0
  170. package/src/cli/terminal/encoding.ts +23 -0
  171. package/src/cli/terminal/error.ts +14 -0
  172. package/src/cli/terminal/io-node-layer.ts +82 -0
  173. package/src/cli/terminal/io-service.ts +25 -0
  174. package/src/cli/terminal/list.ts +36 -0
  175. package/src/cli/terminal/read.test-utils.ts +48 -0
  176. package/src/cli/terminal/read.test.ts +35 -0
  177. package/src/cli/terminal/read.ts +99 -0
  178. package/src/cli/terminal/scope.ts +35 -0
  179. package/src/cli/terminal/shared.ts +250 -0
  180. package/src/cli/terminal/stream.ts +61 -0
  181. package/src/cli/terminal/wait.test.ts +148 -0
  182. package/src/cli/terminal/wait.ts +223 -0
  183. package/src/cli/terminal/write.test.ts +34 -0
  184. package/src/cli/terminal/write.ts +133 -0
  185. package/src/cli/terminal.ts +26 -0
  186. package/src/cli/thread.ts +8 -0
  187. package/src/cli/threads/approve.ts +11 -7
  188. package/src/cli/threads/archive.ts +23 -25
  189. package/src/cli/threads/callback.ts +6 -4
  190. package/src/cli/threads/delete.ts +67 -0
  191. package/src/cli/threads/interrupt.ts +57 -0
  192. package/src/cli/threads/list.test.ts +71 -0
  193. package/src/cli/threads/list.ts +25 -13
  194. package/src/cli/threads/messages.ts +11 -7
  195. package/src/cli/threads/respond.ts +11 -7
  196. package/src/cli/threads/send.ts +39 -9
  197. package/src/cli/threads/show.ts +11 -7
  198. package/src/cli/threads/start.ts +18 -13
  199. package/src/cli/threads/unarchive.ts +48 -0
  200. package/src/cli/threads/update.ts +155 -0
  201. package/src/cli/threads/wait.ts +11 -7
  202. package/src/cli/wait-events.ts +1 -1
  203. package/src/config/config.ts +260 -0
  204. package/src/config/credential/cipher-node.ts +40 -0
  205. package/src/config/credential/cipher-web.ts +82 -0
  206. package/src/config/credential/cipher.ts +39 -0
  207. package/src/config/credential/env.ts +13 -0
  208. package/src/config/credential/error.ts +9 -0
  209. package/src/config/credential/index.ts +6 -0
  210. package/src/config/credential/service.test-utils.ts +25 -0
  211. package/src/config/credential/service.test.ts +116 -0
  212. package/src/config/credential/service.ts +205 -0
  213. package/src/config/env/env.test-utils.ts +14 -0
  214. package/src/config/env/env.ts +81 -0
  215. package/src/config/env/index.ts +2 -0
  216. package/src/{layout/base-dir.ts → config/env/layout.ts} +0 -9
  217. package/src/config/environment-name/index.ts +7 -0
  218. package/src/config/environment-name/name.test.ts +66 -0
  219. package/src/config/environment-name/name.ts +49 -0
  220. package/src/config/error.ts +6 -8
  221. package/src/config/index.ts +16 -5
  222. package/src/config/keystore/error.ts +9 -0
  223. package/src/config/keystore/file.ts +96 -0
  224. package/src/config/keystore/index.ts +4 -0
  225. package/src/config/keystore/keyring-node.test.ts +12 -0
  226. package/src/config/keystore/keyring-node.ts +100 -0
  227. package/src/config/keystore/service.test-utils.ts +15 -0
  228. package/src/config/keystore/service.test.ts +18 -0
  229. package/src/config/keystore/service.ts +29 -0
  230. package/src/config/layer.test-utils.ts +45 -0
  231. package/src/config/layer.test.ts +209 -0
  232. package/src/config/paths/index.ts +1 -0
  233. package/src/config/paths/paths.ts +36 -0
  234. package/src/config/persist/file-mode.ts +21 -0
  235. package/src/config/persist/migration.test.ts +71 -0
  236. package/src/config/persist/migration.ts +82 -0
  237. package/src/config/persist/persist.test.ts +113 -0
  238. package/src/config/persist/persist.ts +65 -0
  239. package/src/config/persist/schema.ts +37 -0
  240. package/src/config/platform.test-utils.ts +20 -0
  241. package/src/config/resolve/resolve.test-utils.ts +20 -0
  242. package/src/config/resolve/resolve.test.ts +137 -0
  243. package/src/config/resolve/resolve.ts +104 -0
  244. package/src/config/selection/index.ts +1 -0
  245. package/src/config/selection/resolve.ts +14 -0
  246. package/src/config/selection/service.ts +29 -0
  247. package/src/config/temp-home.test-utils.ts +7 -0
  248. package/src/config/types.ts +49 -0
  249. package/src/config/url/error.ts +8 -0
  250. package/src/config/url/index.ts +7 -0
  251. package/src/config/url/url.test.ts +31 -0
  252. package/src/connection/index.ts +0 -1
  253. package/src/contracts/index.ts +1 -1
  254. package/src/domain/error.ts +39 -1
  255. package/src/domain/helpers.test.ts +1 -1
  256. package/src/domain/helpers.ts +1 -1
  257. package/src/domain/model-config.ts +5 -1
  258. package/src/domain/thread-activities.test.ts +1 -1
  259. package/src/domain/thread-activities.ts +1 -1
  260. package/src/domain/thread-lifecycle.test.ts +1 -1
  261. package/src/domain/thread-lifecycle.ts +15 -1
  262. package/src/effect.test-utils.ts +25 -0
  263. package/src/index.ts +1 -1
  264. package/src/{connection/node.ts → node/connection.ts} +1 -1
  265. package/src/node/index.ts +1 -0
  266. package/src/orchestration/index.ts +1 -1
  267. package/src/orchestration/layer.ts +34 -86
  268. package/src/orchestration/service.ts +5 -1
  269. package/src/rpc/error.ts +12 -2
  270. package/src/rpc/index.ts +5 -1
  271. package/src/rpc/layer.ts +1 -1
  272. package/src/rpc/operation.ts +83 -0
  273. package/src/rpc/ws-group.ts +25 -1
  274. package/src/runtime/index.ts +2 -1
  275. package/src/runtime/layer.test-utils.ts +32 -0
  276. package/src/runtime/layer.test.ts +66 -0
  277. package/src/runtime/layer.ts +22 -11
  278. package/src/t3tools/index.ts +1 -1
  279. package/src/types/marked-terminal.d.ts +11 -0
  280. package/dist/layout.js +0 -2
  281. package/dist/scope.js +0 -2
  282. package/dist/src/cli/output-format.d.ts +0 -9
  283. package/dist/src/config/layer.d.ts +0 -22
  284. package/dist/src/config/service.d.ts +0 -22
  285. package/dist/src/environment/layer.d.ts +0 -3
  286. package/dist/src/environment/service.d.ts +0 -12
  287. package/dist/src/layout/index.d.ts +0 -1
  288. package/dist/upstream-t3code/packages/contracts/src/auth.d.ts +0 -441
  289. package/dist/upstream-t3code/packages/contracts/src/baseSchemas.d.ts +0 -38
  290. package/dist/upstream-t3code/packages/contracts/src/desktopBootstrap.d.ts +0 -14
  291. package/dist/upstream-t3code/packages/contracts/src/editor.d.ts +0 -124
  292. package/dist/upstream-t3code/packages/contracts/src/environment.d.ts +0 -64
  293. package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +0 -785
  294. package/dist/upstream-t3code/packages/contracts/src/filesystem.d.ts +0 -26
  295. package/dist/upstream-t3code/packages/contracts/src/git.d.ts +0 -500
  296. package/dist/upstream-t3code/packages/contracts/src/index.d.ts +0 -25
  297. package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +0 -496
  298. package/dist/upstream-t3code/packages/contracts/src/keybindings.d.ts +0 -133
  299. package/dist/upstream-t3code/packages/contracts/src/model.d.ts +0 -112
  300. package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +0 -7338
  301. package/dist/upstream-t3code/packages/contracts/src/project.d.ts +0 -45
  302. package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +0 -130
  303. package/dist/upstream-t3code/packages/contracts/src/providerInstance.d.ts +0 -99
  304. package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +0 -4450
  305. package/dist/upstream-t3code/packages/contracts/src/relay.d.ts +0 -1262
  306. package/dist/upstream-t3code/packages/contracts/src/relayClient.d.ts +0 -48
  307. package/dist/upstream-t3code/packages/contracts/src/remoteAccess.d.ts +0 -45
  308. package/dist/upstream-t3code/packages/contracts/src/review.d.ts +0 -37
  309. package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +0 -8710
  310. package/dist/upstream-t3code/packages/contracts/src/server.d.ts +0 -2336
  311. package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +0 -292
  312. package/dist/upstream-t3code/packages/contracts/src/sourceControl.d.ts +0 -177
  313. package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +0 -333
  314. package/dist/upstream-t3code/packages/contracts/src/vcs.d.ts +0 -148
  315. package/src/cli/auth-format.ts +0 -45
  316. package/src/cli/model-format.ts +0 -18
  317. package/src/cli/output-format.ts +0 -41
  318. package/src/cli/project-format.ts +0 -11
  319. package/src/cli/thread-format.ts +0 -121
  320. package/src/config/layer.ts +0 -105
  321. package/src/config/service.ts +0 -26
  322. package/src/environment/layer.ts +0 -12
  323. package/src/environment/service.ts +0 -13
  324. package/src/layout/index.ts +0 -1
  325. /package/dist/src/{scope → cli/scope}/index.d.ts +0 -0
  326. /package/dist/src/config/{url.d.ts → url/url.d.ts} +0 -0
  327. /package/src/{scope → cli/scope}/index.ts +0 -0
  328. /package/src/config/{url.ts → url/url.ts} +0 -0
@@ -0,0 +1,66 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import * as Effect from "effect/Effect";
4
+ import * as Exit from "effect/Exit";
5
+ import { assert, describe, it } from "@effect/vitest";
6
+
7
+ import { expectFailWithMessage } from "../../effect.test-utils.ts";
8
+ import { ConfigError } from "../error.ts";
9
+ import {
10
+ defaultEnvironmentNameForLocal,
11
+ defaultEnvironmentNameFromUrl,
12
+ migrateV1EnvironmentName,
13
+ slugifyEnvironmentName,
14
+ validateEnvironmentName,
15
+ } from "./name.ts";
16
+
17
+ describe("slugifyEnvironmentName", () => {
18
+ it("slugifies invalid characters", () => {
19
+ assert.equal(slugifyEnvironmentName("app.example.com"), "app.example.com");
20
+ assert.equal(slugifyEnvironmentName("---"), "default");
21
+ });
22
+ });
23
+
24
+ describe("validateEnvironmentName", () => {
25
+ it.effect("rejects empty and invalid names", () =>
26
+ Effect.gen(function* () {
27
+ const empty = yield* validateEnvironmentName("").pipe(Effect.exit);
28
+ assert.equal(Exit.isFailure(empty), true);
29
+ const invalid = yield* validateEnvironmentName("bad name").pipe(Effect.exit);
30
+ assert.equal(Exit.isFailure(invalid), true);
31
+ yield* validateEnvironmentName("valid.name-1");
32
+ }),
33
+ );
34
+ });
35
+
36
+ describe("defaultEnvironmentNameFromUrl", () => {
37
+ it.effect("derives hostname from normalized url", () =>
38
+ Effect.gen(function* () {
39
+ const name = yield* defaultEnvironmentNameFromUrl("https://app.example.com/path");
40
+ assert.equal(name, "app.example.com");
41
+ }),
42
+ );
43
+
44
+ it.effect("maps invalid urls to ConfigError", () =>
45
+ expectFailWithMessage(defaultEnvironmentNameFromUrl("not-a-url"), ConfigError, "invalid url"),
46
+ );
47
+ });
48
+
49
+ describe("migrateV1EnvironmentName", () => {
50
+ it.effect("uses local name for local configs", () =>
51
+ Effect.gen(function* () {
52
+ const name = yield* migrateV1EnvironmentName({ local: true });
53
+ assert.equal(name, defaultEnvironmentNameForLocal());
54
+ }),
55
+ );
56
+
57
+ it.effect("derives name from url when present", () =>
58
+ Effect.gen(function* () {
59
+ const name = yield* migrateV1EnvironmentName({
60
+ url: "https://work.example",
61
+ local: false,
62
+ });
63
+ assert.equal(name, "work.example");
64
+ }),
65
+ );
66
+ });
@@ -0,0 +1,49 @@
1
+ import * as Effect from "effect/Effect";
2
+
3
+ import { ConfigError } from "../error.ts";
4
+ import { normalizeHttpBaseUrl } from "../url/url.ts";
5
+
6
+ const environmentNamePattern = /^[A-Za-z0-9._-]+$/;
7
+
8
+ export function slugifyEnvironmentName(value: string) {
9
+ const slug = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
10
+ return slug.length > 0 ? slug : "default";
11
+ }
12
+
13
+ export function validateEnvironmentName(name: string) {
14
+ if (name.length === 0 || !environmentNamePattern.test(name)) {
15
+ return Effect.fail(
16
+ new ConfigError({
17
+ message: `invalid environment name '${name}': use non-empty [A-Za-z0-9._-]`,
18
+ }),
19
+ );
20
+ }
21
+ return Effect.void;
22
+ }
23
+
24
+ export function defaultEnvironmentNameFromUrl(url: string) {
25
+ return normalizeHttpBaseUrl(url).pipe(
26
+ Effect.mapError((error) => new ConfigError({ message: error.message, cause: error.cause })),
27
+ Effect.flatMap((normalized) => {
28
+ const hostname = slugifyEnvironmentName(new URL(normalized).hostname.trim());
29
+ return validateEnvironmentName(hostname).pipe(Effect.as(hostname));
30
+ }),
31
+ );
32
+ }
33
+
34
+ export function defaultEnvironmentNameForLocal() {
35
+ return "local" as const;
36
+ }
37
+
38
+ export function migrateV1EnvironmentName(input: {
39
+ readonly url?: string;
40
+ readonly local?: boolean;
41
+ }) {
42
+ if (input.local === true) {
43
+ return Effect.succeed(defaultEnvironmentNameForLocal());
44
+ }
45
+ if (input.url !== undefined && input.url.length > 0) {
46
+ return defaultEnvironmentNameFromUrl(input.url);
47
+ }
48
+ return Effect.succeed("default");
49
+ }
@@ -1,21 +1,19 @@
1
- import * as Cause from "effect/Cause";
2
1
  import * as Schema from "effect/Schema";
3
2
  import { PlatformError } from "effect/PlatformError";
4
3
 
5
4
  const ConfigErrorCauseSchema = Schema.Union([
6
5
  Schema.instanceOf(PlatformError),
7
6
  Schema.instanceOf(Schema.SchemaError),
7
+ Schema.Defect(),
8
8
  ]);
9
9
 
10
- export class UrlError extends Schema.TaggedErrorClass<UrlError>()("UrlError", {
11
- message: Schema.String,
12
- protocol: Schema.optionalKey(Schema.String),
13
- cause: Schema.optionalKey(Schema.instanceOf(Cause.IllegalArgumentError)),
14
- }) {}
15
-
16
10
  export class ConfigError extends Schema.TaggedErrorClass<ConfigError>()("ConfigError", {
17
11
  message: Schema.String,
18
12
  cause: Schema.optionalKey(ConfigErrorCauseSchema),
19
13
  }) {}
20
14
 
21
- export type ConfigServiceError = ConfigError | UrlError;
15
+ export type ConfigServiceError = ConfigError;
16
+
17
+ export function isPlatformNotFoundError(error: PlatformError) {
18
+ return error.reason["_tag"] === "NotFound";
19
+ }
@@ -1,5 +1,16 @@
1
- export { T3Config } from "./service.ts";
2
- export type { ResolvedConfig, StoredConfig } from "./service.ts";
3
- export { T3ConfigLive, makeT3Config } from "./layer.ts";
4
- export { ConfigError, UrlError } from "./error.ts";
5
- export type { ConfigServiceError } from "./error.ts";
1
+ export * as Config from "./config.ts";
2
+ export type {
3
+ EncryptedConfig,
4
+ EncryptedToken,
5
+ EnvironmentSummary,
6
+ ResolvedConfig,
7
+ UpsertEnvironmentInput,
8
+ } from "./types.ts";
9
+ export { ConfigError, type ConfigServiceError } from "./error.ts";
10
+ export * as Credential from "./credential/index.ts";
11
+ export * as Keystore from "./keystore/index.ts";
12
+ export * as Selection from "./selection/index.ts";
13
+ export * as Env from "./env/index.ts";
14
+ export * as Paths from "./paths/index.ts";
15
+ export * as Url from "./url/index.ts";
16
+ export * as EnvironmentName from "./environment-name/index.ts";
@@ -0,0 +1,9 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ export class KeystoreUnavailableError extends Schema.TaggedErrorClass<KeystoreUnavailableError>()(
4
+ "KeystoreUnavailableError",
5
+ {
6
+ reason: Schema.Literals(["module-not-found", "backend-unavailable"]),
7
+ cause: Schema.Defect(),
8
+ },
9
+ ) {}
@@ -0,0 +1,96 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Encoding from "effect/Encoding";
3
+ import * as FileSystem from "effect/FileSystem";
4
+ import * as Path from "effect/Path";
5
+
6
+ import { ConfigError, isPlatformNotFoundError } from "../error.ts";
7
+ import {
8
+ masterKeyByteLength,
9
+ type MasterKeyKeystore,
10
+ type MasterKeyReadResult,
11
+ } from "./service.ts";
12
+ import { resolveKeyFilePath } from "../paths/paths.ts";
13
+
14
+ const privateFileMode = 0o600;
15
+
16
+ export const makeFileKeystore = Effect.fn("makeFileKeystore")(function* () {
17
+ const fs = yield* FileSystem.FileSystem;
18
+ const path = yield* Path.Path;
19
+ const keyFilePath = yield* resolveKeyFilePath();
20
+
21
+ const read = (): Effect.Effect<MasterKeyReadResult, ConfigError> =>
22
+ Effect.gen(function* () {
23
+ const raw = yield* fs.readFileString(keyFilePath).pipe(
24
+ Effect.catchTags({
25
+ PlatformError: (error) =>
26
+ isPlatformNotFoundError(error)
27
+ ? Effect.succeed(undefined)
28
+ : Effect.fail(
29
+ new ConfigError({ message: "failed to read credential key file", cause: error }),
30
+ ),
31
+ }),
32
+ );
33
+ if (raw === undefined) {
34
+ return { kind: "missing" };
35
+ }
36
+ const key = yield* Effect.fromResult(Encoding.decodeBase64(raw.trim())).pipe(
37
+ Effect.catchTags({
38
+ EncodingError: (error) =>
39
+ Effect.fail(
40
+ new ConfigError({
41
+ message: "invalid credential key file: invalid base64",
42
+ cause: error,
43
+ }),
44
+ ),
45
+ }),
46
+ );
47
+ if (key.byteLength !== masterKeyByteLength) {
48
+ return yield* Effect.fail(
49
+ new ConfigError({ message: "invalid credential key file: unexpected key length" }),
50
+ );
51
+ }
52
+ yield* fs.chmod(keyFilePath, privateFileMode).pipe(
53
+ Effect.mapError(
54
+ (error) =>
55
+ new ConfigError({
56
+ message: "failed to set credential key file permissions",
57
+ cause: error,
58
+ }),
59
+ ),
60
+ );
61
+ return { kind: "present", key };
62
+ });
63
+
64
+ const write = (key: Uint8Array): Effect.Effect<void, ConfigError> =>
65
+ Effect.gen(function* () {
66
+ yield* fs.makeDirectory(path.dirname(keyFilePath), { recursive: true, mode: 0o700 }).pipe(
67
+ Effect.catchTags({
68
+ PlatformError: (error) =>
69
+ Effect.fail(
70
+ new ConfigError({ message: "failed to write credential key file", cause: error }),
71
+ ),
72
+ }),
73
+ );
74
+ yield* fs
75
+ .writeFileString(keyFilePath, `${Encoding.encodeBase64(key)}\n`, { mode: privateFileMode })
76
+ .pipe(
77
+ Effect.catchTags({
78
+ PlatformError: (error) =>
79
+ Effect.fail(
80
+ new ConfigError({ message: "failed to write credential key file", cause: error }),
81
+ ),
82
+ }),
83
+ );
84
+ yield* fs.chmod(keyFilePath, privateFileMode).pipe(
85
+ Effect.mapError(
86
+ (error) =>
87
+ new ConfigError({
88
+ message: "failed to set credential key file permissions",
89
+ cause: error,
90
+ }),
91
+ ),
92
+ );
93
+ });
94
+
95
+ return { read, write } satisfies MasterKeyKeystore;
96
+ });
@@ -0,0 +1,4 @@
1
+ export * as Service from "./service.ts";
2
+ export { KeystoreUnavailableError } from "./error.ts";
3
+ export * as KeyringNode from "./keyring-node.ts";
4
+ export * as File from "./file.ts";
@@ -0,0 +1,12 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import { assert, describe, it } from "@effect/vitest";
4
+
5
+ import { parseKeyringPassword } from "./keyring-node.ts";
6
+
7
+ describe("parseKeyringPassword", () => {
8
+ it("treats invalid stored keyring values as corrupt", () => {
9
+ const result = parseKeyringPassword("not-a-valid-key");
10
+ assert.equal(result.kind, "corrupt");
11
+ });
12
+ });
@@ -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
+ }