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,260 @@
1
+ import * as Context from "effect/Context";
2
+ import * as ConfigProvider from "effect/ConfigProvider";
3
+ import * as Effect from "effect/Effect";
4
+ import * as FileSystem from "effect/FileSystem";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Option from "effect/Option";
7
+ import * as Path from "effect/Path";
8
+
9
+ import { credentialTokenFromEnv } from "./credential/env.ts";
10
+ import { T3CredentialCrypto } from "./credential/service.ts";
11
+ import { loadT3CliEnv } from "./env/env.ts";
12
+ import { validateEnvironmentName } from "./environment-name/name.ts";
13
+ import { ConfigError } from "./error.ts";
14
+ import { readEncryptedConfigFile, writeEncryptedConfigFile } from "./persist/persist.ts";
15
+ import {
16
+ buildResolvedConfigFromEnv,
17
+ buildResolvedConfigFromStored,
18
+ resolveDefaultForUpsert,
19
+ selectEnvironmentName,
20
+ summarizeEnvironments,
21
+ validateCredentialEnvVars,
22
+ } from "./resolve/resolve.ts";
23
+ import { T3ConfigSelection } from "./selection/service.ts";
24
+ import type { EnvironmentSummary, ResolvedConfig, UpsertEnvironmentInput } from "./types.ts";
25
+ import { normalizeHttpBaseUrl } from "./url/url.ts";
26
+
27
+ export class T3Config extends Context.Service<
28
+ T3Config,
29
+ {
30
+ readonly resolve: () => Effect.Effect<ResolvedConfig, ConfigError>;
31
+ readonly resolveActiveEnvironmentName: () => Effect.Effect<string | undefined, ConfigError>;
32
+ readonly listEnvironments: () => Effect.Effect<readonly EnvironmentSummary[], ConfigError>;
33
+ readonly upsertEnvironment: (input: UpsertEnvironmentInput) => Effect.Effect<void, ConfigError>;
34
+ readonly setDefaultEnvironment: (name: string) => Effect.Effect<void, ConfigError>;
35
+ readonly removeEnvironment: (name: string) => Effect.Effect<void, ConfigError>;
36
+ readonly hasEnvironment: (name: string) => Effect.Effect<boolean, ConfigError>;
37
+ readonly getDefaultEnvironmentName: () => Effect.Effect<string | undefined, ConfigError>;
38
+ }
39
+ >()("t3cli/T3Config") {}
40
+
41
+ export const make = Effect.fn("makeT3Config")(function* () {
42
+ const configSelection = yield* T3ConfigSelection;
43
+ const credentialCrypto = yield* T3CredentialCrypto;
44
+ const t3CliEnv = yield* loadT3CliEnv;
45
+ const fs = yield* FileSystem.FileSystem;
46
+ const path = yield* Path.Path;
47
+ const configProvider = yield* ConfigProvider.ConfigProvider;
48
+ const services = Layer.mergeAll(
49
+ Layer.succeed(FileSystem.FileSystem, fs),
50
+ Layer.succeed(Path.Path, path),
51
+ Layer.succeed(T3CredentialCrypto, credentialCrypto),
52
+ Layer.succeed(ConfigProvider.ConfigProvider, configProvider),
53
+ );
54
+
55
+ const hasEnvironment = Effect.fn("T3Config.hasEnvironment")(function* (name: string) {
56
+ const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
57
+ return encrypted.environments[name] !== undefined;
58
+ });
59
+
60
+ const getDefaultEnvironmentName = Effect.fn("T3Config.getDefaultEnvironmentName")(function* () {
61
+ const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
62
+ const defaultName = encrypted.default;
63
+ return defaultName !== undefined && defaultName.length > 0 ? defaultName : undefined;
64
+ });
65
+
66
+ const resolveActiveEnvironmentName = Effect.fn("T3Config.resolveActiveEnvironmentName")(
67
+ function* () {
68
+ const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
69
+ const envUrl = Option.getOrUndefined(t3CliEnv.t3codeUrl);
70
+ const envToken = Option.getOrUndefined(credentialTokenFromEnv(t3CliEnv));
71
+ yield* validateCredentialEnvVars({ envUrl, envToken });
72
+ const hasEnvCredentials = envUrl !== undefined && envToken !== undefined;
73
+ if (hasEnvCredentials) {
74
+ return undefined;
75
+ }
76
+ const configuredEnvironment = yield* configSelection.getSelectedEnvironment();
77
+ const selectedName = selectEnvironmentName({
78
+ selectedEnvironment: configuredEnvironment,
79
+ defaultEnvironment: encrypted.default,
80
+ });
81
+ if (selectedName === undefined || selectedName.length === 0) {
82
+ return undefined;
83
+ }
84
+ if (encrypted.environments[selectedName] === undefined) {
85
+ return undefined;
86
+ }
87
+ return selectedName;
88
+ },
89
+ );
90
+
91
+ const listEnvironments = Effect.fn("T3Config.listEnvironments")(function* () {
92
+ const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
93
+ return summarizeEnvironments(encrypted);
94
+ });
95
+
96
+ const upsertEnvironment = Effect.fn("T3Config.upsertEnvironment")(function* (
97
+ input: UpsertEnvironmentInput,
98
+ ) {
99
+ yield* validateEnvironmentName(input.name);
100
+ const normalizedUrl = yield* normalizeHttpBaseUrl(input.url).pipe(
101
+ Effect.mapError((error) => new ConfigError({ message: error.message, cause: error.cause })),
102
+ );
103
+ const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
104
+ const token = yield* credentialCrypto.encrypt({
105
+ environmentName: input.name,
106
+ url: normalizedUrl,
107
+ local: input.local,
108
+ token: input.token,
109
+ });
110
+ const defaultName = resolveDefaultForUpsert(encrypted, input.name, input.makeDefault);
111
+ yield* Effect.provide(
112
+ writeEncryptedConfigFile({
113
+ version: 2,
114
+ ...(defaultName !== undefined ? { default: defaultName } : {}),
115
+ environments: {
116
+ ...encrypted.environments,
117
+ [input.name]: {
118
+ url: normalizedUrl,
119
+ local: input.local,
120
+ token,
121
+ },
122
+ },
123
+ }),
124
+ services,
125
+ );
126
+ });
127
+
128
+ const setDefaultEnvironment = Effect.fn("T3Config.setDefaultEnvironment")(function* (
129
+ name: string,
130
+ ) {
131
+ yield* validateEnvironmentName(name);
132
+ const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
133
+ if (encrypted.environments[name] === undefined) {
134
+ return yield* Effect.fail(new ConfigError({ message: `environment not found: ${name}` }));
135
+ }
136
+ const selectedEnvironment = encrypted.environments[name];
137
+ yield* credentialCrypto
138
+ .decrypt({
139
+ environmentName: name,
140
+ url: selectedEnvironment.url,
141
+ local: selectedEnvironment.local,
142
+ token: selectedEnvironment.token,
143
+ })
144
+ .pipe(
145
+ Effect.mapError(
146
+ (error) =>
147
+ new ConfigError({
148
+ message: `failed to decrypt credentials for environment '${name}'`,
149
+ cause: error.cause,
150
+ }),
151
+ ),
152
+ Effect.asVoid,
153
+ );
154
+ yield* Effect.provide(
155
+ writeEncryptedConfigFile({
156
+ ...encrypted,
157
+ default: name,
158
+ }),
159
+ services,
160
+ );
161
+ return yield* Effect.void;
162
+ });
163
+
164
+ const removeEnvironment = Effect.fn("T3Config.removeEnvironment")(function* (name: string) {
165
+ yield* validateEnvironmentName(name);
166
+ const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
167
+ if (encrypted.environments[name] === undefined) {
168
+ return yield* Effect.fail(new ConfigError({ message: `environment not found: ${name}` }));
169
+ }
170
+ const { [name]: _removed, ...environments } = encrypted.environments;
171
+ const defaultName = encrypted.default === name ? undefined : encrypted.default;
172
+ yield* Effect.provide(
173
+ writeEncryptedConfigFile({
174
+ version: 2,
175
+ ...(defaultName !== undefined ? { default: defaultName } : {}),
176
+ environments,
177
+ }),
178
+ services,
179
+ );
180
+ return yield* Effect.void;
181
+ });
182
+
183
+ const resolve = Effect.fn("T3Config.resolve")(function* () {
184
+ const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
185
+ const envUrl = Option.getOrUndefined(t3CliEnv.t3codeUrl);
186
+ const envToken = Option.getOrUndefined(credentialTokenFromEnv(t3CliEnv));
187
+ yield* validateCredentialEnvVars({ envUrl, envToken });
188
+ const hasEnvUrl = envUrl !== undefined;
189
+ const configuredEnvironment = yield* configSelection.getSelectedEnvironment();
190
+ const selectedName = selectEnvironmentName({
191
+ selectedEnvironment: configuredEnvironment,
192
+ defaultEnvironment: encrypted.default,
193
+ });
194
+
195
+ if (hasEnvUrl && envToken !== undefined) {
196
+ if (selectedName !== undefined && selectedName.length > 0) {
197
+ yield* validateEnvironmentName(selectedName);
198
+ if (encrypted.environments[selectedName] === undefined) {
199
+ return yield* Effect.fail(
200
+ new ConfigError({ message: `environment not found: ${selectedName}` }),
201
+ );
202
+ }
203
+ }
204
+ return yield* buildResolvedConfigFromEnv({
205
+ envUrl,
206
+ envToken,
207
+ });
208
+ }
209
+
210
+ if (selectedName === undefined || selectedName.length === 0) {
211
+ return yield* Effect.fail(
212
+ new ConfigError({
213
+ message: "not authenticated. run: t3cli auth pair --url <pairing-url>",
214
+ }),
215
+ );
216
+ }
217
+ yield* validateEnvironmentName(selectedName);
218
+ if (encrypted.environments[selectedName] === undefined) {
219
+ return yield* Effect.fail(
220
+ new ConfigError({ message: `environment not found: ${selectedName}` }),
221
+ );
222
+ }
223
+
224
+ const selectedEnvironment = encrypted.environments[selectedName];
225
+ const token = yield* credentialCrypto
226
+ .decrypt({
227
+ environmentName: selectedName,
228
+ url: selectedEnvironment.url,
229
+ local: selectedEnvironment.local,
230
+ token: selectedEnvironment.token,
231
+ })
232
+ .pipe(
233
+ Effect.mapError(
234
+ (error) =>
235
+ new ConfigError({
236
+ message: `failed to decrypt credentials for environment '${selectedName}'`,
237
+ cause: error.cause,
238
+ }),
239
+ ),
240
+ );
241
+ return yield* buildResolvedConfigFromStored({
242
+ selectedName,
243
+ token,
244
+ encrypted,
245
+ });
246
+ });
247
+
248
+ return {
249
+ resolve,
250
+ resolveActiveEnvironmentName,
251
+ listEnvironments,
252
+ upsertEnvironment,
253
+ setDefaultEnvironment,
254
+ removeEnvironment,
255
+ hasEnvironment,
256
+ getDefaultEnvironmentName,
257
+ } as const;
258
+ });
259
+
260
+ export const layer = Layer.effect(T3Config, make());
@@ -0,0 +1,40 @@
1
+ import { createCipheriv, createDecipheriv } from "node:crypto";
2
+
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+
6
+ import { credentialCipherTagByteLength, T3CredentialCipher } from "./cipher.ts";
7
+ import { CredentialCipherError } from "./error.ts";
8
+
9
+ const make = (): T3CredentialCipher["Service"] => ({
10
+ encrypt: (input) =>
11
+ Effect.try({
12
+ try: () => {
13
+ const cipher = createCipheriv("aes-256-gcm", input.key, input.nonce, {
14
+ authTagLength: credentialCipherTagByteLength,
15
+ });
16
+ cipher.setAAD(input.additionalData);
17
+ const ciphertext = Buffer.concat([cipher.update(input.plaintext), cipher.final()]);
18
+ return {
19
+ ciphertext: new Uint8Array(ciphertext),
20
+ tag: new Uint8Array(cipher.getAuthTag()),
21
+ };
22
+ },
23
+ catch: (cause) => new CredentialCipherError({ operation: "encrypt", cause }),
24
+ }),
25
+ decrypt: (input) =>
26
+ Effect.try({
27
+ try: () => {
28
+ const decipher = createDecipheriv("aes-256-gcm", input.key, input.nonce, {
29
+ authTagLength: credentialCipherTagByteLength,
30
+ });
31
+ decipher.setAAD(input.additionalData);
32
+ decipher.setAuthTag(input.tag);
33
+ const plaintext = Buffer.concat([decipher.update(input.ciphertext), decipher.final()]);
34
+ return new Uint8Array(plaintext);
35
+ },
36
+ catch: (cause) => new CredentialCipherError({ operation: "decrypt", cause }),
37
+ }),
38
+ });
39
+
40
+ export const layerNode = Layer.succeed(T3CredentialCipher, make());
@@ -0,0 +1,82 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+
4
+ import { credentialCipherTagByteLength, T3CredentialCipher } from "./cipher.ts";
5
+ import { CredentialCipherError } from "./error.ts";
6
+
7
+ const aesGcmAlgorithm = "AES-GCM";
8
+
9
+ function copyBytes(bytes: Uint8Array) {
10
+ const copy = new Uint8Array(bytes.byteLength);
11
+ copy.set(bytes);
12
+ return copy;
13
+ }
14
+
15
+ function importAesGcmKey(key: Uint8Array, operation: "encrypt" | "decrypt") {
16
+ return Effect.tryPromise({
17
+ try: () =>
18
+ globalThis.crypto.subtle.importKey("raw", copyBytes(key), { name: aesGcmAlgorithm }, false, [
19
+ "encrypt",
20
+ "decrypt",
21
+ ]),
22
+ catch: (cause) => new CredentialCipherError({ operation, cause }),
23
+ });
24
+ }
25
+
26
+ const make = (): T3CredentialCipher["Service"] => ({
27
+ encrypt: (input) =>
28
+ Effect.gen(function* () {
29
+ const cryptoKey = yield* importAesGcmKey(input.key, "encrypt");
30
+ const encrypted = yield* Effect.tryPromise({
31
+ try: () =>
32
+ globalThis.crypto.subtle.encrypt(
33
+ {
34
+ name: aesGcmAlgorithm,
35
+ iv: copyBytes(input.nonce),
36
+ additionalData: copyBytes(input.additionalData),
37
+ tagLength: credentialCipherTagByteLength * 8,
38
+ },
39
+ cryptoKey,
40
+ copyBytes(input.plaintext),
41
+ ),
42
+ catch: (cause) => new CredentialCipherError({ operation: "encrypt", cause }),
43
+ });
44
+ const bytes = new Uint8Array(encrypted);
45
+ if (bytes.byteLength < credentialCipherTagByteLength) {
46
+ return yield* Effect.fail(
47
+ new CredentialCipherError({
48
+ operation: "encrypt",
49
+ cause: new Error("encrypted output shorter than auth tag"),
50
+ }),
51
+ );
52
+ }
53
+ return {
54
+ ciphertext: bytes.slice(0, -credentialCipherTagByteLength),
55
+ tag: bytes.slice(-credentialCipherTagByteLength),
56
+ };
57
+ }),
58
+ decrypt: (input) =>
59
+ Effect.gen(function* () {
60
+ const cryptoKey = yield* importAesGcmKey(input.key, "decrypt");
61
+ const combined = new Uint8Array(input.ciphertext.byteLength + input.tag.byteLength);
62
+ combined.set(input.ciphertext);
63
+ combined.set(input.tag, input.ciphertext.byteLength);
64
+ const plaintext = yield* Effect.tryPromise({
65
+ try: () =>
66
+ globalThis.crypto.subtle.decrypt(
67
+ {
68
+ name: aesGcmAlgorithm,
69
+ iv: copyBytes(input.nonce),
70
+ additionalData: copyBytes(input.additionalData),
71
+ tagLength: credentialCipherTagByteLength * 8,
72
+ },
73
+ cryptoKey,
74
+ combined,
75
+ ),
76
+ catch: (cause) => new CredentialCipherError({ operation: "decrypt", cause }),
77
+ });
78
+ return new Uint8Array(plaintext);
79
+ }),
80
+ });
81
+
82
+ export const layerWeb = Layer.succeed(T3CredentialCipher, make());
@@ -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
+ });