t3code-cli 0.11.0 → 0.13.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 (330) 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 +85549 -2668
  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/rolldown-runtime.js +1 -1
  12. package/dist/rpc.js +1 -1
  13. package/dist/runtime.js +2 -2
  14. package/dist/shared.js +23313 -8022
  15. package/dist/src/application/actions.d.ts +25 -0
  16. package/dist/src/application/index.d.ts +3 -3
  17. package/dist/src/application/layer.d.ts +66 -50
  18. package/dist/src/application/model-selection.d.ts +2 -2
  19. package/dist/src/application/models.d.ts +1 -1
  20. package/dist/src/application/project-commands.d.ts +23 -3
  21. package/dist/src/application/projects.d.ts +7 -7
  22. package/dist/src/application/service.d.ts +65 -2
  23. package/dist/src/application/shell-sequence.d.ts +25 -19
  24. package/dist/src/application/terminals.d.ts +8 -8
  25. package/dist/src/application/thread-commands.d.ts +19 -19
  26. package/dist/src/application/thread-update.d.ts +1 -1
  27. package/dist/src/application/thread-wait.d.ts +19 -15
  28. package/dist/src/application/threads.d.ts +134 -108
  29. package/dist/src/auth/error.d.ts +3 -3
  30. package/dist/src/auth/index.d.ts +1 -1
  31. package/dist/src/auth/layer.d.ts +36 -8
  32. package/dist/src/auth/local-base-dir.d.ts +5 -2
  33. package/dist/src/auth/local-origin.d.ts +3 -3
  34. package/dist/src/auth/local-token.d.ts +3 -3
  35. package/dist/src/auth/remote-error.d.ts +3 -0
  36. package/dist/src/auth/service.d.ts +15 -3
  37. package/dist/src/auth/type.d.ts +40 -1
  38. package/dist/src/cli/env/flag.d.ts +2 -0
  39. package/dist/src/cli/env/index.d.ts +2 -0
  40. package/dist/src/cli/env/selection-layer.d.ts +3 -0
  41. package/dist/src/cli/flags.d.ts +2 -0
  42. package/dist/src/cli/format/index.d.ts +1 -0
  43. package/dist/src/cli/format/output.d.ts +12 -0
  44. package/dist/src/cli/index.d.ts +4 -0
  45. package/dist/src/cli/runtime/index.d.ts +1 -0
  46. package/dist/src/cli/runtime/service.d.ts +16 -0
  47. package/dist/src/{scope → cli/scope}/resolve.d.ts +5 -4
  48. package/dist/src/config/config.d.ts +49 -0
  49. package/dist/src/config/credential/cipher-node.d.ts +3 -0
  50. package/dist/src/config/credential/cipher-web.d.ts +3 -0
  51. package/dist/src/config/credential/cipher.d.ts +29 -0
  52. package/dist/src/config/credential/env.d.ts +3 -0
  53. package/dist/src/config/credential/error.d.ts +8 -0
  54. package/dist/src/config/credential/index.d.ts +6 -0
  55. package/dist/src/config/credential/service.d.ts +34 -0
  56. package/dist/src/config/env/env.d.ts +55 -0
  57. package/dist/src/config/env/index.d.ts +2 -0
  58. package/dist/src/{layout/base-dir.d.ts → config/env/layout.d.ts} +0 -1
  59. package/dist/src/config/environment-name/index.d.ts +1 -0
  60. package/dist/src/config/environment-name/name.d.ts +10 -0
  61. package/dist/src/config/error.d.ts +4 -11
  62. package/dist/src/config/index.d.ts +10 -5
  63. package/dist/src/config/keystore/error.d.ts +8 -0
  64. package/dist/src/config/keystore/file.d.ts +9 -0
  65. package/dist/src/config/keystore/index.d.ts +4 -0
  66. package/dist/src/config/keystore/keyring-node.d.ts +4 -0
  67. package/dist/src/config/keystore/service.d.ts +33 -0
  68. package/dist/src/config/paths/index.d.ts +1 -0
  69. package/dist/src/config/paths/paths.d.ts +6 -0
  70. package/dist/src/config/persist/file-mode.d.ts +4 -0
  71. package/dist/src/config/persist/migration.d.ts +68 -0
  72. package/dist/src/config/persist/persist.d.ts +40 -0
  73. package/dist/src/config/persist/schema.d.ts +68 -0
  74. package/dist/src/config/resolve/resolve.d.ts +40 -0
  75. package/dist/src/config/selection/index.d.ts +1 -0
  76. package/dist/src/config/selection/resolve.d.ts +4 -0
  77. package/dist/src/config/selection/service.d.ts +10 -0
  78. package/dist/src/config/types.d.ts +36 -0
  79. package/dist/src/config/url/error.d.ts +10 -0
  80. package/dist/src/config/url/index.d.ts +2 -0
  81. package/dist/src/connection/error.d.ts +3 -1
  82. package/dist/src/connection/index.d.ts +0 -1
  83. package/dist/src/connection/prepared.d.ts +14 -0
  84. package/dist/src/contracts/index.d.ts +1 -1
  85. package/dist/src/domain/error.d.ts +14 -1
  86. package/dist/src/domain/helpers.d.ts +53 -43
  87. package/dist/src/domain/model-config.d.ts +92 -84
  88. package/dist/src/domain/thread-activities.d.ts +1 -1
  89. package/dist/src/domain/thread-lifecycle.d.ts +24 -20
  90. package/dist/src/index.d.ts +1 -2
  91. package/dist/src/{connection/node.d.ts → node/connection.d.ts} +1 -1
  92. package/dist/src/node/index.d.ts +1 -2
  93. package/dist/src/orchestration/index.d.ts +0 -1
  94. package/dist/src/orchestration/layer.d.ts +321 -390
  95. package/dist/src/orchestration/service.d.ts +1 -1
  96. package/dist/src/rpc/error.d.ts +4 -6
  97. package/dist/src/rpc/layer.d.ts +2036 -1917
  98. package/dist/src/rpc/operation.d.ts +4 -5732
  99. package/dist/src/rpc/service.d.ts +1 -1
  100. package/dist/src/rpc/session.d.ts +22 -0
  101. package/dist/src/rpc/ws-group.d.ts +456 -1032
  102. package/dist/src/runtime/index.d.ts +1 -2
  103. package/dist/src/runtime/layer.d.ts +12 -8
  104. package/dist/src/sql/node-sqlite-client.d.ts +1 -1
  105. package/dist/src/t3tools/index.d.ts +1 -1
  106. package/dist/t3tools.js +2 -2
  107. package/package.json +30 -25
  108. package/src/application/actions.test.ts +328 -0
  109. package/src/application/actions.ts +475 -0
  110. package/src/application/index.ts +9 -5
  111. package/src/application/layer.ts +9 -0
  112. package/src/application/model-selection.ts +2 -2
  113. package/src/application/project-commands.ts +23 -5
  114. package/src/application/projects.ts +6 -7
  115. package/src/application/service.ts +79 -1
  116. package/src/application/shell-sequence.ts +1 -1
  117. package/src/application/terminals.ts +2 -1
  118. package/src/application/thread-commands.ts +1 -1
  119. package/src/application/thread-update.ts +1 -1
  120. package/src/application/thread-wait.ts +5 -1
  121. package/src/application/threads.test.ts +9 -3
  122. package/src/application/threads.ts +6 -6
  123. package/src/auth/error.ts +4 -9
  124. package/src/auth/index.ts +7 -0
  125. package/src/auth/layer.test-utils.ts +34 -0
  126. package/src/auth/layer.ts +116 -34
  127. package/src/auth/local-base-dir.ts +13 -6
  128. package/src/auth/local-origin.ts +10 -7
  129. package/src/auth/local-token.ts +28 -23
  130. package/src/auth/remote-error.ts +17 -0
  131. package/src/auth/service.test.ts +200 -0
  132. package/src/auth/service.ts +27 -3
  133. package/src/auth/transport.ts +45 -114
  134. package/src/auth/type.ts +47 -1
  135. package/src/bin.ts +24 -6
  136. package/src/cli/action-format.ts +110 -0
  137. package/src/cli/action.ts +398 -0
  138. package/src/cli/app.ts +6 -0
  139. package/src/cli/auth.ts +76 -23
  140. package/src/cli/env/flag.ts +8 -0
  141. package/src/cli/env/index.ts +2 -0
  142. package/src/cli/env/selection-layer.test.ts +49 -0
  143. package/src/cli/env/selection-layer.ts +27 -0
  144. package/src/cli/env.ts +107 -0
  145. package/src/cli/extra-args.test.ts +32 -0
  146. package/src/cli/extra-args.ts +22 -0
  147. package/src/cli/flags.ts +11 -1
  148. package/src/cli/format/auth.ts +92 -0
  149. package/src/cli/format/human.ts +131 -0
  150. package/src/cli/format/index.ts +12 -0
  151. package/src/cli/format/model.ts +29 -0
  152. package/src/cli/format/output.ts +51 -0
  153. package/src/cli/format/project.ts +32 -0
  154. package/src/cli/format/terminal.ts +87 -0
  155. package/src/cli/{thread-format.test.ts → format/thread.test.ts} +6 -4
  156. package/src/cli/format/thread.ts +185 -0
  157. package/src/cli/index.ts +20 -0
  158. package/src/cli/interaction/confirm.ts +64 -0
  159. package/src/cli/{self-action.ts → interaction/self-action.ts} +8 -6
  160. package/src/cli/message-input.ts +1 -1
  161. package/src/cli/model.ts +9 -5
  162. package/src/cli/project.ts +13 -7
  163. package/src/cli/projects/delete.ts +13 -12
  164. package/src/cli/require.ts +9 -7
  165. package/src/cli/runtime/index.ts +1 -0
  166. package/src/cli/runtime/service.ts +28 -0
  167. package/src/{scope → cli/scope}/resolve.ts +11 -9
  168. package/src/cli/terminal/attach.ts +3 -6
  169. package/src/cli/terminal/create.ts +10 -9
  170. package/src/cli/terminal/destroy.ts +13 -11
  171. package/src/cli/terminal/list.ts +10 -9
  172. package/src/cli/terminal/read.test-utils.ts +48 -0
  173. package/src/cli/terminal/read.test.ts +35 -0
  174. package/src/cli/terminal/read.ts +3 -6
  175. package/src/cli/terminal/scope.ts +10 -5
  176. package/src/cli/terminal/shared.ts +1 -1
  177. package/src/cli/terminal/stream.ts +3 -6
  178. package/src/cli/terminal/wait.test.ts +5 -3
  179. package/src/cli/terminal/wait.ts +10 -9
  180. package/src/cli/terminal/write.test.ts +34 -0
  181. package/src/cli/terminal/write.ts +10 -9
  182. package/src/cli/threads/approve.ts +11 -7
  183. package/src/cli/threads/archive.ts +14 -9
  184. package/src/cli/threads/callback.ts +6 -4
  185. package/src/cli/threads/delete.ts +17 -11
  186. package/src/cli/threads/interrupt.ts +14 -9
  187. package/src/cli/threads/list.test.ts +5 -3
  188. package/src/cli/threads/list.ts +10 -10
  189. package/src/cli/threads/messages.ts +11 -7
  190. package/src/cli/threads/respond.ts +11 -7
  191. package/src/cli/threads/send.ts +19 -9
  192. package/src/cli/threads/show.ts +11 -7
  193. package/src/cli/threads/start.ts +18 -13
  194. package/src/cli/threads/unarchive.ts +11 -7
  195. package/src/cli/threads/update.ts +14 -9
  196. package/src/cli/threads/wait.ts +11 -7
  197. package/src/cli/wait-events.ts +1 -1
  198. package/src/config/config.ts +260 -0
  199. package/src/config/credential/cipher-node.ts +40 -0
  200. package/src/config/credential/cipher-web.ts +82 -0
  201. package/src/config/credential/cipher.ts +39 -0
  202. package/src/config/credential/env.ts +13 -0
  203. package/src/config/credential/error.ts +9 -0
  204. package/src/config/credential/index.ts +6 -0
  205. package/src/config/credential/service.test-utils.ts +25 -0
  206. package/src/config/credential/service.test.ts +116 -0
  207. package/src/config/credential/service.ts +205 -0
  208. package/src/config/env/env.test-utils.ts +14 -0
  209. package/src/config/env/env.ts +81 -0
  210. package/src/config/env/index.ts +2 -0
  211. package/src/{layout/base-dir.ts → config/env/layout.ts} +0 -9
  212. package/src/config/environment-name/index.ts +7 -0
  213. package/src/config/environment-name/name.test.ts +66 -0
  214. package/src/config/environment-name/name.ts +49 -0
  215. package/src/config/error.ts +6 -8
  216. package/src/config/index.ts +16 -5
  217. package/src/config/keystore/error.ts +9 -0
  218. package/src/config/keystore/file.ts +96 -0
  219. package/src/config/keystore/index.ts +4 -0
  220. package/src/config/keystore/keyring-node.test.ts +12 -0
  221. package/src/config/keystore/keyring-node.ts +100 -0
  222. package/src/config/keystore/service.test-utils.ts +15 -0
  223. package/src/config/keystore/service.test.ts +18 -0
  224. package/src/config/keystore/service.ts +29 -0
  225. package/src/config/layer.test-utils.ts +45 -0
  226. package/src/config/layer.test.ts +209 -0
  227. package/src/config/paths/index.ts +1 -0
  228. package/src/config/paths/paths.ts +36 -0
  229. package/src/config/persist/file-mode.ts +21 -0
  230. package/src/config/persist/migration.test.ts +71 -0
  231. package/src/config/persist/migration.ts +82 -0
  232. package/src/config/persist/persist.test.ts +113 -0
  233. package/src/config/persist/persist.ts +65 -0
  234. package/src/config/persist/schema.ts +37 -0
  235. package/src/config/platform.test-utils.ts +20 -0
  236. package/src/config/resolve/resolve.test-utils.ts +20 -0
  237. package/src/config/resolve/resolve.test.ts +137 -0
  238. package/src/config/resolve/resolve.ts +104 -0
  239. package/src/config/selection/index.ts +1 -0
  240. package/src/config/selection/resolve.ts +14 -0
  241. package/src/config/selection/service.ts +29 -0
  242. package/src/config/temp-home.test-utils.ts +7 -0
  243. package/src/config/types.ts +49 -0
  244. package/src/config/url/error.ts +8 -0
  245. package/src/config/url/index.ts +7 -0
  246. package/src/config/url/url.test.ts +31 -0
  247. package/src/connection/error.ts +4 -1
  248. package/src/connection/index.ts +0 -1
  249. package/src/connection/layer.ts +5 -2
  250. package/src/connection/prepared.ts +83 -0
  251. package/src/contracts/index.ts +1 -1
  252. package/src/domain/error.ts +20 -1
  253. package/src/domain/helpers.test.ts +1 -1
  254. package/src/domain/helpers.ts +1 -1
  255. package/src/domain/model-config.ts +1 -1
  256. package/src/domain/thread-activities.test.ts +1 -1
  257. package/src/domain/thread-activities.ts +1 -1
  258. package/src/domain/thread-lifecycle.test.ts +1 -1
  259. package/src/domain/thread-lifecycle.ts +1 -1
  260. package/src/effect.test-utils.ts +25 -0
  261. package/src/index.ts +1 -1
  262. package/src/{connection/node.ts → node/connection.ts} +1 -1
  263. package/src/node/index.ts +1 -2
  264. package/src/orchestration/index.ts +0 -1
  265. package/src/orchestration/layer.ts +3 -2
  266. package/src/orchestration/service.ts +1 -1
  267. package/src/rpc/error.ts +10 -17
  268. package/src/rpc/layer.ts +17 -51
  269. package/src/rpc/operation.ts +16 -2
  270. package/src/rpc/service.ts +2 -1
  271. package/src/rpc/session.ts +168 -0
  272. package/src/rpc/ws-group.ts +13 -27
  273. package/src/runtime/index.ts +2 -1
  274. package/src/runtime/layer.test-utils.ts +32 -0
  275. package/src/runtime/layer.test.ts +66 -0
  276. package/src/runtime/layer.ts +21 -12
  277. package/src/t3tools/index.ts +1 -1
  278. package/src/types/marked-terminal.d.ts +11 -0
  279. package/dist/layout.js +0 -2
  280. package/dist/scope.js +0 -2
  281. package/dist/src/cli/output-format.d.ts +0 -11
  282. package/dist/src/config/layer.d.ts +0 -22
  283. package/dist/src/config/service.d.ts +0 -22
  284. package/dist/src/environment/layer.d.ts +0 -3
  285. package/dist/src/environment/service.d.ts +0 -12
  286. package/dist/src/layout/index.d.ts +0 -1
  287. package/dist/upstream-t3code/packages/contracts/src/auth.d.ts +0 -441
  288. package/dist/upstream-t3code/packages/contracts/src/baseSchemas.d.ts +0 -38
  289. package/dist/upstream-t3code/packages/contracts/src/desktopBootstrap.d.ts +0 -14
  290. package/dist/upstream-t3code/packages/contracts/src/editor.d.ts +0 -124
  291. package/dist/upstream-t3code/packages/contracts/src/environment.d.ts +0 -64
  292. package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +0 -762
  293. package/dist/upstream-t3code/packages/contracts/src/filesystem.d.ts +0 -26
  294. package/dist/upstream-t3code/packages/contracts/src/git.d.ts +0 -500
  295. package/dist/upstream-t3code/packages/contracts/src/index.d.ts +0 -25
  296. package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +0 -490
  297. package/dist/upstream-t3code/packages/contracts/src/keybindings.d.ts +0 -133
  298. package/dist/upstream-t3code/packages/contracts/src/model.d.ts +0 -112
  299. package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +0 -6682
  300. package/dist/upstream-t3code/packages/contracts/src/project.d.ts +0 -45
  301. package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +0 -116
  302. package/dist/upstream-t3code/packages/contracts/src/providerInstance.d.ts +0 -99
  303. package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +0 -4276
  304. package/dist/upstream-t3code/packages/contracts/src/relay.d.ts +0 -1262
  305. package/dist/upstream-t3code/packages/contracts/src/relayClient.d.ts +0 -48
  306. package/dist/upstream-t3code/packages/contracts/src/remoteAccess.d.ts +0 -45
  307. package/dist/upstream-t3code/packages/contracts/src/review.d.ts +0 -37
  308. package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +0 -8218
  309. package/dist/upstream-t3code/packages/contracts/src/server.d.ts +0 -2291
  310. package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +0 -271
  311. package/dist/upstream-t3code/packages/contracts/src/sourceControl.d.ts +0 -177
  312. package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +0 -330
  313. package/dist/upstream-t3code/packages/contracts/src/vcs.d.ts +0 -148
  314. package/src/cli/auth-format.ts +0 -45
  315. package/src/cli/confirm.ts +0 -31
  316. package/src/cli/model-format.ts +0 -18
  317. package/src/cli/output-format.ts +0 -45
  318. package/src/cli/project-format.ts +0 -18
  319. package/src/cli/terminal/commands.test.ts +0 -101
  320. package/src/cli/terminal-format.ts +0 -167
  321. package/src/cli/thread-format.ts +0 -128
  322. package/src/config/layer.ts +0 -105
  323. package/src/config/service.ts +0 -26
  324. package/src/environment/layer.ts +0 -12
  325. package/src/environment/service.ts +0 -13
  326. package/src/layout/index.ts +0 -1
  327. /package/dist/src/{scope → cli/scope}/index.d.ts +0 -0
  328. /package/dist/src/config/{url.d.ts → url/url.d.ts} +0 -0
  329. /package/src/{scope → cli/scope}/index.ts +0 -0
  330. /package/src/config/{url.ts → url/url.ts} +0 -0
@@ -0,0 +1,39 @@
1
+ import * as Context from "effect/Context";
2
+ import type * as Effect from "effect/Effect";
3
+
4
+ import type { CredentialCipherError } from "./error.ts";
5
+
6
+ export const credentialCipherNonceByteLength = 12;
7
+ export const credentialCipherTagByteLength = 16;
8
+
9
+ export type AesGcmEncryptInput = {
10
+ readonly key: Uint8Array;
11
+ readonly nonce: Uint8Array;
12
+ readonly plaintext: Uint8Array;
13
+ readonly additionalData: Uint8Array;
14
+ };
15
+
16
+ export type AesGcmDecryptInput = {
17
+ readonly key: Uint8Array;
18
+ readonly nonce: Uint8Array;
19
+ readonly ciphertext: Uint8Array;
20
+ readonly tag: Uint8Array;
21
+ readonly additionalData: Uint8Array;
22
+ };
23
+
24
+ export type AesGcmEncryptResult = {
25
+ readonly ciphertext: Uint8Array;
26
+ readonly tag: Uint8Array;
27
+ };
28
+
29
+ export class T3CredentialCipher extends Context.Service<
30
+ T3CredentialCipher,
31
+ {
32
+ readonly encrypt: (
33
+ input: AesGcmEncryptInput,
34
+ ) => Effect.Effect<AesGcmEncryptResult, CredentialCipherError>;
35
+ readonly decrypt: (
36
+ input: AesGcmDecryptInput,
37
+ ) => Effect.Effect<Uint8Array, CredentialCipherError>;
38
+ }
39
+ >()("t3cli/T3CredentialCipher") {}
@@ -0,0 +1,13 @@
1
+ import * as Option from "effect/Option";
2
+ import * as Redacted from "effect/Redacted";
3
+
4
+ import type { T3CliEnvShape } from "../env/env.ts";
5
+
6
+ export function credentialTokenFromEnv(
7
+ t3CliEnv: Pick<T3CliEnvShape, "t3codeToken">,
8
+ ): Option.Option<string> {
9
+ return Option.flatMap(t3CliEnv.t3codeToken, (token) => {
10
+ const value = Redacted.value(token).trim();
11
+ return value.length > 0 ? Option.some(value) : Option.none();
12
+ });
13
+ }
@@ -0,0 +1,9 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ export class CredentialCipherError extends Schema.TaggedErrorClass<CredentialCipherError>()(
4
+ "CredentialCipherError",
5
+ {
6
+ operation: Schema.Literals(["encrypt", "decrypt"]),
7
+ cause: Schema.Defect(),
8
+ },
9
+ ) {}
@@ -0,0 +1,6 @@
1
+ export * as Service from "./service.ts";
2
+ export * as Cipher from "./cipher.ts";
3
+ export * as CipherNode from "./cipher-node.ts";
4
+ export * as CipherWeb from "./cipher-web.ts";
5
+ export { credentialTokenFromEnv } from "./env.ts";
6
+ export { CredentialCipherError } from "./error.ts";
@@ -0,0 +1,25 @@
1
+ import * as Layer from "effect/Layer";
2
+
3
+ import { CliRuntime } from "../../cli/runtime/service.ts";
4
+ import { ConfigPlatformLayer } from "../platform.test-utils.ts";
5
+ import { t3CliEnvConfigLayer } from "../env/env.test-utils.ts";
6
+ import * as CredentialCipherWeb from "./cipher-web.ts";
7
+ import * as Keystore from "../keystore/service.test-utils.ts";
8
+ import * as Credential from "./service.ts";
9
+
10
+ export function t3CredentialCryptoDepsLayer(homeDir: string) {
11
+ return Credential.layer.pipe(
12
+ Layer.provide(
13
+ Layer.mergeAll(
14
+ CliRuntime.layerTest({ cwd: homeDir }),
15
+ t3CliEnvConfigLayer(homeDir),
16
+ CredentialCipherWeb.layerWeb,
17
+ Keystore.unavailableKeystoreFactoryLayer,
18
+ ),
19
+ ),
20
+ );
21
+ }
22
+
23
+ export function t3CredentialCryptoLayerTest(homeDir: string) {
24
+ return Layer.mergeAll(ConfigPlatformLayer, t3CredentialCryptoDepsLayer(homeDir));
25
+ }
@@ -0,0 +1,116 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import * as Crypto from "effect/Crypto";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Exit from "effect/Exit";
6
+ import * as FileSystem from "effect/FileSystem";
7
+ import * as Path from "effect/Path";
8
+ import { assert, describe, it } from "@effect/vitest";
9
+
10
+ import { ConfigPlatformLayer } from "../platform.test-utils.ts";
11
+ import { makeTempHomeScoped } from "../temp-home.test-utils.ts";
12
+ import { T3CredentialCrypto } from "./service.ts";
13
+ import { t3CredentialCryptoDepsLayer } from "./service.test-utils.ts";
14
+
15
+ describe("T3CredentialCrypto", () => {
16
+ it.effect("falls back to key file when keyring backend is unavailable", () =>
17
+ Effect.gen(function* () {
18
+ const fs = yield* FileSystem.FileSystem;
19
+ const path = yield* Path.Path;
20
+ const cryptoService = yield* Crypto.Crypto;
21
+ const homeDir = yield* makeTempHomeScoped("t3cli-credential-");
22
+ yield* Effect.gen(function* () {
23
+ const keyPath = path.join(homeDir, ".config", "t3cli", "key");
24
+ const masterKey = yield* cryptoService.randomBytes(32);
25
+ yield* fs.makeDirectory(path.dirname(keyPath), { recursive: true });
26
+ yield* fs.writeFileString(keyPath, `${Buffer.from(masterKey).toString("base64")}\n`, {
27
+ mode: 0o600,
28
+ });
29
+ const credentialCrypto = yield* T3CredentialCrypto;
30
+ const encrypted = yield* credentialCrypto.encrypt({
31
+ environmentName: "home",
32
+ url: "https://home.example",
33
+ local: false,
34
+ token: "secret-token",
35
+ });
36
+ const token = yield* credentialCrypto.decrypt({
37
+ environmentName: "home",
38
+ url: "https://home.example",
39
+ local: false,
40
+ token: encrypted,
41
+ });
42
+ assert.equal(token, "secret-token");
43
+ }).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)));
44
+ }).pipe(Effect.provide(ConfigPlatformLayer), Effect.scoped),
45
+ );
46
+
47
+ it.effect("hardens existing key file permissions to 0600 on use", () =>
48
+ Effect.gen(function* () {
49
+ const fs = yield* FileSystem.FileSystem;
50
+ const path = yield* Path.Path;
51
+ const cryptoService = yield* Crypto.Crypto;
52
+ const homeDir = yield* makeTempHomeScoped("t3cli-credential-");
53
+ yield* Effect.gen(function* () {
54
+ const keyPath = path.join(homeDir, ".config", "t3cli", "key");
55
+ const masterKey = yield* cryptoService.randomBytes(32);
56
+ yield* fs.makeDirectory(path.dirname(keyPath), { recursive: true });
57
+ yield* fs.writeFileString(keyPath, `${Buffer.from(masterKey).toString("base64")}\n`, {
58
+ mode: 0o644,
59
+ });
60
+ const credentialCrypto = yield* T3CredentialCrypto;
61
+ yield* credentialCrypto.encrypt({
62
+ environmentName: "home",
63
+ url: "https://home.example",
64
+ local: false,
65
+ token: "secret-token",
66
+ });
67
+ const keyStat = yield* fs.stat(keyPath);
68
+ assert.equal(keyStat.mode & 0o777, 0o600);
69
+ }).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)));
70
+ }).pipe(Effect.provide(ConfigPlatformLayer), Effect.scoped),
71
+ );
72
+
73
+ it.effect("creates key file with 0600 permissions when keyring is unavailable", () =>
74
+ Effect.gen(function* () {
75
+ const fs = yield* FileSystem.FileSystem;
76
+ const path = yield* Path.Path;
77
+ const homeDir = yield* makeTempHomeScoped("t3cli-credential-");
78
+ yield* Effect.gen(function* () {
79
+ const keyPath = path.join(homeDir, ".config", "t3cli", "key");
80
+ const credentialCrypto = yield* T3CredentialCrypto;
81
+ yield* credentialCrypto.encrypt({
82
+ environmentName: "home",
83
+ url: "https://home.example",
84
+ local: false,
85
+ token: "rotate-write",
86
+ });
87
+ const keyStat = yield* fs.stat(keyPath);
88
+ assert.equal(keyStat.mode & 0o777, 0o600);
89
+ }).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)));
90
+ }).pipe(Effect.provide(ConfigPlatformLayer), Effect.scoped),
91
+ );
92
+
93
+ it.effect("fails decrypt when ciphertext AAD does not match", () =>
94
+ Effect.gen(function* () {
95
+ const homeDir = yield* makeTempHomeScoped("t3cli-credential-");
96
+ yield* Effect.gen(function* () {
97
+ const crypto = yield* T3CredentialCrypto;
98
+ const token = yield* crypto.encrypt({
99
+ environmentName: "home",
100
+ url: "https://home.example",
101
+ local: false,
102
+ token: "secret",
103
+ });
104
+ const result = yield* crypto
105
+ .decrypt({
106
+ environmentName: "home",
107
+ url: "https://tampered.example",
108
+ local: false,
109
+ token,
110
+ })
111
+ .pipe(Effect.exit);
112
+ assert.equal(Exit.isFailure(result), true);
113
+ }).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)));
114
+ }).pipe(Effect.provide(ConfigPlatformLayer), Effect.scoped),
115
+ );
116
+ });
@@ -0,0 +1,205 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Crypto from "effect/Crypto";
3
+ import * as Effect from "effect/Effect";
4
+ import * as ConfigProvider from "effect/ConfigProvider";
5
+ import * as Encoding from "effect/Encoding";
6
+ import * as FileSystem from "effect/FileSystem";
7
+ import * as Layer from "effect/Layer";
8
+ import * as Path from "effect/Path";
9
+
10
+ import { T3CredentialCipher, credentialCipherNonceByteLength } from "./cipher.ts";
11
+ import type { EncryptedToken } from "../persist/schema.ts";
12
+ import { ConfigError } from "../error.ts";
13
+ import { CredentialCipherError } from "./error.ts";
14
+ import { makeFileKeystore } from "../keystore/file.ts";
15
+ import { T3MasterKeyKeystoreFactory, masterKeyByteLength } from "../keystore/service.ts";
16
+
17
+ export type CredentialEncryptInput = {
18
+ readonly environmentName: string;
19
+ readonly url: string;
20
+ readonly local: boolean;
21
+ readonly token: string;
22
+ };
23
+
24
+ export type CredentialDecryptInput = {
25
+ readonly environmentName: string;
26
+ readonly url: string;
27
+ readonly local: boolean;
28
+ readonly token: EncryptedToken;
29
+ };
30
+
31
+ export class T3CredentialCrypto extends Context.Service<
32
+ T3CredentialCrypto,
33
+ {
34
+ readonly encrypt: (input: CredentialEncryptInput) => Effect.Effect<EncryptedToken, ConfigError>;
35
+ readonly decrypt: (input: CredentialDecryptInput) => Effect.Effect<string, ConfigError>;
36
+ }
37
+ >()("t3cli/T3CredentialCrypto") {}
38
+
39
+ const configSchemaVersion = 2;
40
+ const textEncoder = new TextEncoder();
41
+ const textDecoder = new TextDecoder();
42
+
43
+ export const make = Effect.fn("makeT3CredentialCrypto")(function* () {
44
+ const fs = yield* FileSystem.FileSystem;
45
+ const path = yield* Path.Path;
46
+ const configProvider = yield* ConfigProvider.ConfigProvider;
47
+ const cryptoService = yield* Crypto.Crypto;
48
+ const cipher = yield* T3CredentialCipher;
49
+ const keystoreFactory = yield* T3MasterKeyKeystoreFactory;
50
+ const services = Layer.mergeAll(
51
+ Layer.succeed(FileSystem.FileSystem, fs),
52
+ Layer.succeed(Path.Path, path),
53
+ Layer.succeed(Crypto.Crypto, cryptoService),
54
+ Layer.succeed(ConfigProvider.ConfigProvider, configProvider),
55
+ );
56
+
57
+ const fileKeystore = yield* Effect.provide(makeFileKeystore(), services);
58
+ const primaryKeystore = yield* keystoreFactory.make().pipe(
59
+ Effect.catchTags({
60
+ KeystoreUnavailableError: () => Effect.succeed(undefined),
61
+ }),
62
+ );
63
+
64
+ const readMasterKey = Effect.fn("readMasterKey")(function* () {
65
+ if (primaryKeystore !== undefined) {
66
+ const primaryResult = yield* primaryKeystore.read();
67
+ if (primaryResult.kind === "present") {
68
+ return primaryResult.key;
69
+ }
70
+ if (primaryResult.kind === "corrupt") {
71
+ return yield* Effect.fail(
72
+ new ConfigError({
73
+ message: `corrupt credential key in OS keyring: ${primaryResult.message}`,
74
+ }),
75
+ );
76
+ }
77
+ }
78
+
79
+ const fileResult = yield* fileKeystore.read();
80
+ if (fileResult.kind === "present") {
81
+ return fileResult.key;
82
+ }
83
+ return undefined;
84
+ });
85
+
86
+ const writeMasterKey = Effect.fn("writeMasterKey")(function* (key: Uint8Array) {
87
+ if (primaryKeystore !== undefined) {
88
+ yield* primaryKeystore.write(key).pipe(
89
+ Effect.catchTags({
90
+ KeystoreUnavailableError: () => fileKeystore.write(key),
91
+ }),
92
+ );
93
+ return;
94
+ }
95
+ yield* fileKeystore.write(key);
96
+ });
97
+
98
+ const getMasterKey = Effect.fn("getMasterKey")(function* () {
99
+ const existing = yield* readMasterKey();
100
+ if (existing !== undefined) {
101
+ return existing;
102
+ }
103
+ const generated = yield* cryptoService.randomBytes(masterKeyByteLength).pipe(
104
+ Effect.mapError(
105
+ (error) =>
106
+ new ConfigError({
107
+ message: "failed to generate secure random bytes",
108
+ cause: error,
109
+ }),
110
+ ),
111
+ );
112
+ yield* writeMasterKey(generated);
113
+ return generated;
114
+ });
115
+
116
+ const encrypt: (input: CredentialEncryptInput) => Effect.Effect<EncryptedToken, ConfigError> =
117
+ Effect.fn("encrypt")(function* (input) {
118
+ const masterKey = yield* getMasterKey();
119
+ const nonce = yield* cryptoService.randomBytes(credentialCipherNonceByteLength).pipe(
120
+ Effect.mapError(
121
+ (error) =>
122
+ new ConfigError({
123
+ message: "failed to generate secure random bytes",
124
+ cause: error,
125
+ }),
126
+ ),
127
+ );
128
+ const encrypted = yield* cipher
129
+ .encrypt({
130
+ key: masterKey,
131
+ nonce,
132
+ plaintext: encodeUtf8(input.token),
133
+ additionalData: buildCredentialAad(input),
134
+ })
135
+ .pipe(Effect.mapError(mapCredentialCipherError));
136
+ return {
137
+ kind: "encrypted" as const,
138
+ alg: "aes-256-gcm" as const,
139
+ key: "default" as const,
140
+ nonce: Encoding.encodeBase64(nonce),
141
+ ciphertext: Encoding.encodeBase64(encrypted.ciphertext),
142
+ tag: Encoding.encodeBase64(encrypted.tag),
143
+ };
144
+ });
145
+
146
+ const decrypt: (input: CredentialDecryptInput) => Effect.Effect<string, ConfigError> = Effect.fn(
147
+ "decrypt",
148
+ )(function* (input) {
149
+ const masterKey = yield* getMasterKey();
150
+ const nonce = yield* decodeBase64Field(input.token.nonce, "token nonce");
151
+ const ciphertext = yield* decodeBase64Field(input.token.ciphertext, "token ciphertext");
152
+ const tag = yield* decodeBase64Field(input.token.tag, "token tag");
153
+ const plaintext = yield* cipher
154
+ .decrypt({
155
+ key: masterKey,
156
+ nonce,
157
+ ciphertext,
158
+ tag,
159
+ additionalData: buildCredentialAad(input),
160
+ })
161
+ .pipe(Effect.mapError(mapCredentialCipherError));
162
+ return decodeUtf8(plaintext);
163
+ });
164
+
165
+ return { encrypt, decrypt } satisfies T3CredentialCrypto["Service"];
166
+ });
167
+
168
+ export const layer = Layer.effect(T3CredentialCrypto, make());
169
+
170
+ function buildCredentialAad(input: {
171
+ readonly environmentName: string;
172
+ readonly url: string;
173
+ readonly local: boolean;
174
+ }) {
175
+ return encodeUtf8(
176
+ `${configSchemaVersion}\0${input.environmentName}\0${input.url}\0${input.local}`,
177
+ );
178
+ }
179
+
180
+ function encodeUtf8(value: string) {
181
+ return textEncoder.encode(value);
182
+ }
183
+
184
+ function decodeUtf8(value: Uint8Array) {
185
+ return textDecoder.decode(value);
186
+ }
187
+
188
+ function decodeBase64Field(value: string, field: string) {
189
+ return Effect.fromResult(Encoding.decodeBase64(value)).pipe(
190
+ Effect.catchTags({
191
+ EncodingError: (error) =>
192
+ Effect.fail(new ConfigError({ message: `invalid encrypted token ${field}`, cause: error })),
193
+ }),
194
+ );
195
+ }
196
+
197
+ function mapCredentialCipherError(error: CredentialCipherError) {
198
+ return new ConfigError({
199
+ message:
200
+ error.operation === "encrypt"
201
+ ? "failed to encrypt credential cipher payload"
202
+ : "failed to decrypt credential cipher payload",
203
+ cause: error,
204
+ });
205
+ }
@@ -0,0 +1,14 @@
1
+ import * as ConfigProvider from "effect/ConfigProvider";
2
+
3
+ export function t3CliEnvConfigLayer(
4
+ homeDir: string,
5
+ env: Readonly<Record<string, string | undefined>> = {},
6
+ ) {
7
+ const record: Record<string, string> = { HOME: homeDir };
8
+ for (const [key, value] of Object.entries(env)) {
9
+ if (value !== undefined) {
10
+ record[key] = value;
11
+ }
12
+ }
13
+ return ConfigProvider.layer(ConfigProvider.fromEnv({ env: record }));
14
+ }
@@ -0,0 +1,81 @@
1
+ import * as Config from "effect/Config";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Option from "effect/Option";
4
+ import type * as Redacted from "effect/Redacted";
5
+
6
+ export type T3CliEnvScope = {
7
+ readonly t3codeProjectRoot?: string | undefined;
8
+ readonly t3codeProjectId?: string | undefined;
9
+ readonly t3codeWorktreePath?: string | undefined;
10
+ readonly t3codeThreadId?: string | undefined;
11
+ };
12
+
13
+ export type T3CliEnvShape = {
14
+ readonly home: Option.Option<string>;
15
+ readonly xdgConfigHome: Option.Option<string>;
16
+ readonly t3cliEnv: Option.Option<string>;
17
+ readonly t3codeUrl: Option.Option<string>;
18
+ readonly t3codeToken: Option.Option<Redacted.Redacted>;
19
+ readonly t3codeHome: Option.Option<string>;
20
+ readonly term: Option.Option<string>;
21
+ readonly ci: boolean;
22
+ readonly codexCi: boolean;
23
+ readonly codexThreadId: boolean;
24
+ readonly t3cliAgent: boolean;
25
+ readonly scope: T3CliEnvScope;
26
+ };
27
+
28
+ export const T3CliEnvConfig = Config.all({
29
+ home: Config.string("HOME").pipe(Config.option),
30
+ xdgConfigHome: Config.string("XDG_CONFIG_HOME").pipe(Config.option),
31
+ t3cliEnv: Config.string("T3CLI_ENV").pipe(Config.option),
32
+ t3codeUrl: Config.string("T3CODE_URL").pipe(Config.option),
33
+ t3codeToken: Config.redacted("T3CODE_TOKEN").pipe(Config.option),
34
+ t3codeHome: Config.string("T3CODE_HOME").pipe(Config.option),
35
+ t3codeProjectRoot: Config.string("T3CODE_PROJECT_ROOT").pipe(Config.option),
36
+ t3codeProjectId: Config.string("T3CODE_PROJECT_ID").pipe(Config.option),
37
+ t3codeWorktreePath: Config.string("T3CODE_WORKTREE_PATH").pipe(Config.option),
38
+ t3codeThreadId: Config.string("T3CODE_THREAD_ID").pipe(Config.option),
39
+ term: Config.string("TERM").pipe(Config.option),
40
+ ci: Config.string("CI").pipe(Config.option, Config.map(Option.isSome)),
41
+ codexCi: Config.string("CODEX_CI").pipe(Config.option, Config.map(Option.isSome)),
42
+ codexThreadId: Config.string("CODEX_THREAD_ID").pipe(Config.option, Config.map(Option.isSome)),
43
+ t3cliAgent: Config.string("T3CLI_AGENT").pipe(Config.option, Config.map(Option.isSome)),
44
+ });
45
+
46
+ function toScope(loaded: {
47
+ readonly t3codeProjectRoot: Option.Option<string>;
48
+ readonly t3codeProjectId: Option.Option<string>;
49
+ readonly t3codeWorktreePath: Option.Option<string>;
50
+ readonly t3codeThreadId: Option.Option<string>;
51
+ }): T3CliEnvScope {
52
+ return {
53
+ ...(Option.isSome(loaded.t3codeProjectRoot)
54
+ ? { t3codeProjectRoot: loaded.t3codeProjectRoot.value }
55
+ : {}),
56
+ ...(Option.isSome(loaded.t3codeProjectId)
57
+ ? { t3codeProjectId: loaded.t3codeProjectId.value }
58
+ : {}),
59
+ ...(Option.isSome(loaded.t3codeWorktreePath)
60
+ ? { t3codeWorktreePath: loaded.t3codeWorktreePath.value }
61
+ : {}),
62
+ ...(Option.isSome(loaded.t3codeThreadId)
63
+ ? { t3codeThreadId: loaded.t3codeThreadId.value }
64
+ : {}),
65
+ };
66
+ }
67
+
68
+ export const loadT3CliEnv = Effect.map(T3CliEnvConfig, (loaded) => ({
69
+ home: loaded.home,
70
+ xdgConfigHome: loaded.xdgConfigHome,
71
+ t3cliEnv: loaded.t3cliEnv,
72
+ t3codeUrl: loaded.t3codeUrl,
73
+ t3codeToken: loaded.t3codeToken,
74
+ t3codeHome: loaded.t3codeHome,
75
+ term: loaded.term,
76
+ ci: loaded.ci,
77
+ codexCi: loaded.codexCi,
78
+ codexThreadId: loaded.codexThreadId,
79
+ t3cliAgent: loaded.t3cliAgent,
80
+ scope: toScope(loaded),
81
+ }));
@@ -0,0 +1,2 @@
1
+ export { loadT3CliEnv, T3CliEnvConfig, type T3CliEnvScope, type T3CliEnvShape } from "./env.ts";
2
+ export { resolveT3BaseDir, type T3Layout } from "./layout.ts";
@@ -1,4 +1,3 @@
1
- import { homedir } from "node:os";
2
1
  import * as Effect from "effect/Effect";
3
2
  import * as Path from "effect/Path";
4
3
 
@@ -8,14 +7,6 @@ export type T3Layout = {
8
7
  readonly t3codeHome?: string | undefined;
9
8
  };
10
9
 
11
- export function readT3LayoutFromNodeProcess(): T3Layout {
12
- return {
13
- cwd: process.cwd(),
14
- homeDir: homedir(),
15
- t3codeHome: process.env["T3CODE_HOME"],
16
- };
17
- }
18
-
19
10
  export const resolveT3BaseDir = Effect.fn("resolveT3BaseDir")(function* (input: {
20
11
  readonly layout: T3Layout;
21
12
  readonly baseDir?: string | undefined;
@@ -0,0 +1,7 @@
1
+ export {
2
+ defaultEnvironmentNameForLocal,
3
+ defaultEnvironmentNameFromUrl,
4
+ migrateV1EnvironmentName,
5
+ slugifyEnvironmentName,
6
+ validateEnvironmentName,
7
+ } from "./name.ts";
@@ -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
+ }