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
@@ -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
+ }
@@ -0,0 +1,209 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import * as Effect from "effect/Effect";
4
+ import { assert, describe, it } from "@effect/vitest";
5
+
6
+ import { T3Config } from "./config.ts";
7
+ import { ConfigPlatformLayer } from "./platform.test-utils.ts";
8
+ import { makeTempHomeScoped } from "./temp-home.test-utils.ts";
9
+ import { t3ConfigDepsLayer } from "./layer.test-utils.ts";
10
+
11
+ describe("T3Config", () => {
12
+ it.effect(
13
+ "keeps default unchanged when upserting an existing environment without makeDefault",
14
+ () =>
15
+ Effect.gen(function* () {
16
+ return yield* makeTempHomeScoped("t3cli-config-");
17
+ }).pipe(
18
+ Effect.flatMap((homeDir) =>
19
+ Effect.gen(function* () {
20
+ const config = yield* T3Config;
21
+ yield* config.upsertEnvironment({
22
+ name: "home",
23
+ url: "https://home.example",
24
+ token: "home-token",
25
+ local: false,
26
+ });
27
+ yield* config.upsertEnvironment({
28
+ name: "work",
29
+ url: "https://work.example",
30
+ token: "work-token",
31
+ local: false,
32
+ });
33
+ yield* config.upsertEnvironment({
34
+ name: "work",
35
+ url: "https://work.example",
36
+ token: "work-token-2",
37
+ local: false,
38
+ });
39
+ const listed = yield* config.listEnvironments();
40
+ assert.equal(listed.find((environment) => environment.name === "home")?.default, true);
41
+ assert.equal(listed.find((environment) => environment.name === "work")?.default, false);
42
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir))),
43
+ ),
44
+ Effect.provide(ConfigPlatformLayer),
45
+ Effect.scoped,
46
+ ),
47
+ );
48
+
49
+ it.effect("promotes default when upserting with makeDefault", () =>
50
+ Effect.gen(function* () {
51
+ return yield* makeTempHomeScoped("t3cli-config-");
52
+ }).pipe(
53
+ Effect.flatMap((homeDir) =>
54
+ Effect.gen(function* () {
55
+ const config = yield* T3Config;
56
+ yield* config.upsertEnvironment({
57
+ name: "home",
58
+ url: "https://home.example",
59
+ token: "home-token",
60
+ local: false,
61
+ });
62
+ yield* config.upsertEnvironment({
63
+ name: "work",
64
+ url: "https://work.example",
65
+ token: "work-token",
66
+ local: false,
67
+ });
68
+ yield* config.upsertEnvironment({
69
+ name: "work",
70
+ url: "https://work.example",
71
+ token: "work-token-2",
72
+ local: false,
73
+ makeDefault: true,
74
+ });
75
+ const listed = yield* config.listEnvironments();
76
+ assert.equal(listed.find((environment) => environment.name === "home")?.default, false);
77
+ assert.equal(listed.find((environment) => environment.name === "work")?.default, true);
78
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir))),
79
+ ),
80
+ Effect.provide(ConfigPlatformLayer),
81
+ Effect.scoped,
82
+ ),
83
+ );
84
+
85
+ it.effect("clears default when removing the default environment", () =>
86
+ Effect.gen(function* () {
87
+ return yield* makeTempHomeScoped("t3cli-config-");
88
+ }).pipe(
89
+ Effect.flatMap((homeDir) =>
90
+ Effect.gen(function* () {
91
+ const config = yield* T3Config;
92
+ yield* config.upsertEnvironment({
93
+ name: "home",
94
+ url: "https://home.example",
95
+ token: "home-token",
96
+ local: false,
97
+ });
98
+ yield* config.upsertEnvironment({
99
+ name: "work",
100
+ url: "https://work.example",
101
+ token: "work-token",
102
+ local: false,
103
+ });
104
+ yield* config.removeEnvironment("home");
105
+ const defaultName = yield* config.getDefaultEnvironmentName();
106
+ assert.equal(defaultName, undefined);
107
+ const listed = yield* config.listEnvironments();
108
+ assert.equal(
109
+ listed.every((environment) => !environment.default),
110
+ true,
111
+ );
112
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir))),
113
+ ),
114
+ Effect.provide(ConfigPlatformLayer),
115
+ Effect.scoped,
116
+ ),
117
+ );
118
+
119
+ it.effect("resolves selected environment from config selection service", () =>
120
+ Effect.gen(function* () {
121
+ return yield* makeTempHomeScoped("t3cli-config-");
122
+ }).pipe(
123
+ Effect.flatMap((homeDir) =>
124
+ Effect.gen(function* () {
125
+ const config = yield* T3Config;
126
+ yield* config.upsertEnvironment({
127
+ name: "home",
128
+ url: "https://home.example",
129
+ token: "home-token",
130
+ local: false,
131
+ });
132
+ yield* config.upsertEnvironment({
133
+ name: "work",
134
+ url: "https://work.example",
135
+ token: "work-token",
136
+ local: true,
137
+ });
138
+ const resolved = yield* config.resolve();
139
+ assert.equal(resolved.source, "config");
140
+ if (resolved.source === "config") {
141
+ assert.equal(resolved.environment, "work");
142
+ assert.equal(resolved.token, "work-token");
143
+ }
144
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir, { selection: "work" }))),
145
+ ),
146
+ Effect.provide(ConfigPlatformLayer),
147
+ Effect.scoped,
148
+ ),
149
+ );
150
+
151
+ it.effect(
152
+ "resolves env override with local=false even when selected stored environment is local",
153
+ () =>
154
+ Effect.gen(function* () {
155
+ return yield* makeTempHomeScoped("t3cli-config-");
156
+ }).pipe(
157
+ Effect.flatMap((homeDir) =>
158
+ Effect.gen(function* () {
159
+ const config = yield* T3Config;
160
+ yield* config.upsertEnvironment({
161
+ name: "work",
162
+ url: "http://localhost:8787",
163
+ token: "work-token",
164
+ local: true,
165
+ });
166
+ const resolved = yield* config.resolve();
167
+ assert.equal(resolved.source, "env");
168
+ assert.equal(resolved.local, false);
169
+ assert.equal(resolved.url, "https://remote.example");
170
+ assert.equal(resolved.token, "env-token");
171
+ }).pipe(
172
+ Effect.provide(
173
+ t3ConfigDepsLayer(homeDir, {
174
+ selection: "work",
175
+ env: {
176
+ T3CODE_URL: "https://remote.example",
177
+ T3CODE_TOKEN: "env-token",
178
+ },
179
+ }),
180
+ ),
181
+ ),
182
+ ),
183
+ Effect.provide(ConfigPlatformLayer),
184
+ Effect.scoped,
185
+ ),
186
+ );
187
+
188
+ it.effect("reads default environment name without decrypting tokens", () =>
189
+ Effect.gen(function* () {
190
+ return yield* makeTempHomeScoped("t3cli-config-");
191
+ }).pipe(
192
+ Effect.flatMap((homeDir) =>
193
+ Effect.gen(function* () {
194
+ const config = yield* T3Config;
195
+ yield* config.upsertEnvironment({
196
+ name: "home",
197
+ url: "https://home.example",
198
+ token: "home-token",
199
+ local: false,
200
+ });
201
+ const defaultName = yield* config.getDefaultEnvironmentName();
202
+ assert.equal(defaultName, "home");
203
+ }).pipe(Effect.provide(t3ConfigDepsLayer(homeDir))),
204
+ ),
205
+ Effect.provide(ConfigPlatformLayer),
206
+ Effect.scoped,
207
+ ),
208
+ );
209
+ });
@@ -0,0 +1 @@
1
+ export { resolveConfigFilePath, resolveKeyFilePath, resolveT3cliConfigDir } from "./paths.ts";
@@ -0,0 +1,36 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Option from "effect/Option";
3
+ import * as Path from "effect/Path";
4
+
5
+ import { loadT3CliEnv } from "../env/env.ts";
6
+ import { ConfigError } from "../error.ts";
7
+
8
+ export const resolveT3cliConfigDir = Effect.fn("resolveT3cliConfigDir")(function* () {
9
+ const path = yield* Path.Path;
10
+ const t3CliEnv = yield* loadT3CliEnv.pipe(
11
+ Effect.mapError(
12
+ (error) => new ConfigError({ message: "failed to load CLI environment", cause: error }),
13
+ ),
14
+ );
15
+ const home = yield* Option.match(t3CliEnv.home, {
16
+ onNone: () => Effect.die("HOME is not set"),
17
+ onSome: Effect.succeed,
18
+ });
19
+ const root = Option.match(t3CliEnv.xdgConfigHome, {
20
+ onNone: () => path.join(home, ".config"),
21
+ onSome: (xdgConfigHome) => xdgConfigHome,
22
+ });
23
+ return path.join(root, "t3cli");
24
+ });
25
+
26
+ export const resolveConfigFilePath = Effect.fn("resolveConfigFilePath")(function* () {
27
+ const path = yield* Path.Path;
28
+ const configDir = yield* resolveT3cliConfigDir();
29
+ return path.join(configDir, "config.json");
30
+ });
31
+
32
+ export const resolveKeyFilePath = Effect.fn("resolveKeyFilePath")(function* () {
33
+ const path = yield* Path.Path;
34
+ const configDir = yield* resolveT3cliConfigDir();
35
+ return path.join(configDir, "key");
36
+ });
@@ -0,0 +1,21 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+
4
+ import { ConfigError } from "../error.ts";
5
+
6
+ const privateFileMode = 0o600;
7
+
8
+ export const hardenPrivateFileMode = Effect.fn("hardenPrivateFileMode")(function* (
9
+ filePath: string,
10
+ label: "config" | "credential key",
11
+ ) {
12
+ const fs = yield* FileSystem.FileSystem;
13
+ yield* fs
14
+ .chmod(filePath, privateFileMode)
15
+ .pipe(
16
+ Effect.mapError(
17
+ (error) =>
18
+ new ConfigError({ message: `failed to set ${label} file permissions`, cause: error }),
19
+ ),
20
+ );
21
+ });