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.
- package/README.md +42 -10
- package/dist/application.js +2 -2
- package/dist/auth.js +1 -1
- package/dist/bin.js +85549 -2668
- package/dist/cli.js +15 -2
- package/dist/config.js +120 -2
- package/dist/connection.js +2 -2
- package/dist/index.js +1 -1
- package/dist/node.js +6 -2
- package/dist/orchestration.js +2 -2
- package/dist/rolldown-runtime.js +1 -1
- package/dist/rpc.js +1 -1
- package/dist/runtime.js +2 -2
- package/dist/shared.js +23313 -8022
- package/dist/src/application/actions.d.ts +25 -0
- package/dist/src/application/index.d.ts +3 -3
- package/dist/src/application/layer.d.ts +66 -50
- package/dist/src/application/model-selection.d.ts +2 -2
- package/dist/src/application/models.d.ts +1 -1
- package/dist/src/application/project-commands.d.ts +23 -3
- package/dist/src/application/projects.d.ts +7 -7
- package/dist/src/application/service.d.ts +65 -2
- package/dist/src/application/shell-sequence.d.ts +25 -19
- package/dist/src/application/terminals.d.ts +8 -8
- package/dist/src/application/thread-commands.d.ts +19 -19
- package/dist/src/application/thread-update.d.ts +1 -1
- package/dist/src/application/thread-wait.d.ts +19 -15
- package/dist/src/application/threads.d.ts +134 -108
- package/dist/src/auth/error.d.ts +3 -3
- package/dist/src/auth/index.d.ts +1 -1
- package/dist/src/auth/layer.d.ts +36 -8
- package/dist/src/auth/local-base-dir.d.ts +5 -2
- package/dist/src/auth/local-origin.d.ts +3 -3
- package/dist/src/auth/local-token.d.ts +3 -3
- package/dist/src/auth/remote-error.d.ts +3 -0
- package/dist/src/auth/service.d.ts +15 -3
- package/dist/src/auth/type.d.ts +40 -1
- package/dist/src/cli/env/flag.d.ts +2 -0
- package/dist/src/cli/env/index.d.ts +2 -0
- package/dist/src/cli/env/selection-layer.d.ts +3 -0
- package/dist/src/cli/flags.d.ts +2 -0
- package/dist/src/cli/format/index.d.ts +1 -0
- package/dist/src/cli/format/output.d.ts +12 -0
- package/dist/src/cli/index.d.ts +4 -0
- package/dist/src/cli/runtime/index.d.ts +1 -0
- package/dist/src/cli/runtime/service.d.ts +16 -0
- package/dist/src/{scope → cli/scope}/resolve.d.ts +5 -4
- package/dist/src/config/config.d.ts +49 -0
- package/dist/src/config/credential/cipher-node.d.ts +3 -0
- package/dist/src/config/credential/cipher-web.d.ts +3 -0
- package/dist/src/config/credential/cipher.d.ts +29 -0
- package/dist/src/config/credential/env.d.ts +3 -0
- package/dist/src/config/credential/error.d.ts +8 -0
- package/dist/src/config/credential/index.d.ts +6 -0
- package/dist/src/config/credential/service.d.ts +34 -0
- package/dist/src/config/env/env.d.ts +55 -0
- package/dist/src/config/env/index.d.ts +2 -0
- package/dist/src/{layout/base-dir.d.ts → config/env/layout.d.ts} +0 -1
- package/dist/src/config/environment-name/index.d.ts +1 -0
- package/dist/src/config/environment-name/name.d.ts +10 -0
- package/dist/src/config/error.d.ts +4 -11
- package/dist/src/config/index.d.ts +10 -5
- package/dist/src/config/keystore/error.d.ts +8 -0
- package/dist/src/config/keystore/file.d.ts +9 -0
- package/dist/src/config/keystore/index.d.ts +4 -0
- package/dist/src/config/keystore/keyring-node.d.ts +4 -0
- package/dist/src/config/keystore/service.d.ts +33 -0
- package/dist/src/config/paths/index.d.ts +1 -0
- package/dist/src/config/paths/paths.d.ts +6 -0
- package/dist/src/config/persist/file-mode.d.ts +4 -0
- package/dist/src/config/persist/migration.d.ts +68 -0
- package/dist/src/config/persist/persist.d.ts +40 -0
- package/dist/src/config/persist/schema.d.ts +68 -0
- package/dist/src/config/resolve/resolve.d.ts +40 -0
- package/dist/src/config/selection/index.d.ts +1 -0
- package/dist/src/config/selection/resolve.d.ts +4 -0
- package/dist/src/config/selection/service.d.ts +10 -0
- package/dist/src/config/types.d.ts +36 -0
- package/dist/src/config/url/error.d.ts +10 -0
- package/dist/src/config/url/index.d.ts +2 -0
- package/dist/src/connection/error.d.ts +3 -1
- package/dist/src/connection/index.d.ts +0 -1
- package/dist/src/connection/prepared.d.ts +14 -0
- package/dist/src/contracts/index.d.ts +1 -1
- package/dist/src/domain/error.d.ts +14 -1
- package/dist/src/domain/helpers.d.ts +53 -43
- package/dist/src/domain/model-config.d.ts +92 -84
- package/dist/src/domain/thread-activities.d.ts +1 -1
- package/dist/src/domain/thread-lifecycle.d.ts +24 -20
- package/dist/src/index.d.ts +1 -2
- package/dist/src/{connection/node.d.ts → node/connection.d.ts} +1 -1
- package/dist/src/node/index.d.ts +1 -2
- package/dist/src/orchestration/index.d.ts +0 -1
- package/dist/src/orchestration/layer.d.ts +321 -390
- package/dist/src/orchestration/service.d.ts +1 -1
- package/dist/src/rpc/error.d.ts +4 -6
- package/dist/src/rpc/layer.d.ts +2036 -1917
- package/dist/src/rpc/operation.d.ts +4 -5732
- package/dist/src/rpc/service.d.ts +1 -1
- package/dist/src/rpc/session.d.ts +22 -0
- package/dist/src/rpc/ws-group.d.ts +456 -1032
- package/dist/src/runtime/index.d.ts +1 -2
- package/dist/src/runtime/layer.d.ts +12 -8
- package/dist/src/sql/node-sqlite-client.d.ts +1 -1
- package/dist/src/t3tools/index.d.ts +1 -1
- package/dist/t3tools.js +2 -2
- package/package.json +30 -25
- package/src/application/actions.test.ts +328 -0
- package/src/application/actions.ts +475 -0
- package/src/application/index.ts +9 -5
- package/src/application/layer.ts +9 -0
- package/src/application/model-selection.ts +2 -2
- package/src/application/project-commands.ts +23 -5
- package/src/application/projects.ts +6 -7
- package/src/application/service.ts +79 -1
- package/src/application/shell-sequence.ts +1 -1
- package/src/application/terminals.ts +2 -1
- package/src/application/thread-commands.ts +1 -1
- package/src/application/thread-update.ts +1 -1
- package/src/application/thread-wait.ts +5 -1
- package/src/application/threads.test.ts +9 -3
- package/src/application/threads.ts +6 -6
- package/src/auth/error.ts +4 -9
- package/src/auth/index.ts +7 -0
- package/src/auth/layer.test-utils.ts +34 -0
- package/src/auth/layer.ts +116 -34
- package/src/auth/local-base-dir.ts +13 -6
- package/src/auth/local-origin.ts +10 -7
- package/src/auth/local-token.ts +28 -23
- package/src/auth/remote-error.ts +17 -0
- package/src/auth/service.test.ts +200 -0
- package/src/auth/service.ts +27 -3
- package/src/auth/transport.ts +45 -114
- package/src/auth/type.ts +47 -1
- package/src/bin.ts +24 -6
- package/src/cli/action-format.ts +110 -0
- package/src/cli/action.ts +398 -0
- package/src/cli/app.ts +6 -0
- package/src/cli/auth.ts +76 -23
- package/src/cli/env/flag.ts +8 -0
- package/src/cli/env/index.ts +2 -0
- package/src/cli/env/selection-layer.test.ts +49 -0
- package/src/cli/env/selection-layer.ts +27 -0
- package/src/cli/env.ts +107 -0
- package/src/cli/extra-args.test.ts +32 -0
- package/src/cli/extra-args.ts +22 -0
- package/src/cli/flags.ts +11 -1
- package/src/cli/format/auth.ts +92 -0
- package/src/cli/format/human.ts +131 -0
- package/src/cli/format/index.ts +12 -0
- package/src/cli/format/model.ts +29 -0
- package/src/cli/format/output.ts +51 -0
- package/src/cli/format/project.ts +32 -0
- package/src/cli/format/terminal.ts +87 -0
- package/src/cli/{thread-format.test.ts → format/thread.test.ts} +6 -4
- package/src/cli/format/thread.ts +185 -0
- package/src/cli/index.ts +20 -0
- package/src/cli/interaction/confirm.ts +64 -0
- package/src/cli/{self-action.ts → interaction/self-action.ts} +8 -6
- package/src/cli/message-input.ts +1 -1
- package/src/cli/model.ts +9 -5
- package/src/cli/project.ts +13 -7
- package/src/cli/projects/delete.ts +13 -12
- package/src/cli/require.ts +9 -7
- package/src/cli/runtime/index.ts +1 -0
- package/src/cli/runtime/service.ts +28 -0
- package/src/{scope → cli/scope}/resolve.ts +11 -9
- package/src/cli/terminal/attach.ts +3 -6
- package/src/cli/terminal/create.ts +10 -9
- package/src/cli/terminal/destroy.ts +13 -11
- package/src/cli/terminal/list.ts +10 -9
- package/src/cli/terminal/read.test-utils.ts +48 -0
- package/src/cli/terminal/read.test.ts +35 -0
- package/src/cli/terminal/read.ts +3 -6
- package/src/cli/terminal/scope.ts +10 -5
- package/src/cli/terminal/shared.ts +1 -1
- package/src/cli/terminal/stream.ts +3 -6
- package/src/cli/terminal/wait.test.ts +5 -3
- package/src/cli/terminal/wait.ts +10 -9
- package/src/cli/terminal/write.test.ts +34 -0
- package/src/cli/terminal/write.ts +10 -9
- package/src/cli/threads/approve.ts +11 -7
- package/src/cli/threads/archive.ts +14 -9
- package/src/cli/threads/callback.ts +6 -4
- package/src/cli/threads/delete.ts +17 -11
- package/src/cli/threads/interrupt.ts +14 -9
- package/src/cli/threads/list.test.ts +5 -3
- package/src/cli/threads/list.ts +10 -10
- package/src/cli/threads/messages.ts +11 -7
- package/src/cli/threads/respond.ts +11 -7
- package/src/cli/threads/send.ts +19 -9
- package/src/cli/threads/show.ts +11 -7
- package/src/cli/threads/start.ts +18 -13
- package/src/cli/threads/unarchive.ts +11 -7
- package/src/cli/threads/update.ts +14 -9
- package/src/cli/threads/wait.ts +11 -7
- package/src/cli/wait-events.ts +1 -1
- package/src/config/config.ts +260 -0
- package/src/config/credential/cipher-node.ts +40 -0
- package/src/config/credential/cipher-web.ts +82 -0
- package/src/config/credential/cipher.ts +39 -0
- package/src/config/credential/env.ts +13 -0
- package/src/config/credential/error.ts +9 -0
- package/src/config/credential/index.ts +6 -0
- package/src/config/credential/service.test-utils.ts +25 -0
- package/src/config/credential/service.test.ts +116 -0
- package/src/config/credential/service.ts +205 -0
- package/src/config/env/env.test-utils.ts +14 -0
- package/src/config/env/env.ts +81 -0
- package/src/config/env/index.ts +2 -0
- package/src/{layout/base-dir.ts → config/env/layout.ts} +0 -9
- package/src/config/environment-name/index.ts +7 -0
- package/src/config/environment-name/name.test.ts +66 -0
- package/src/config/environment-name/name.ts +49 -0
- package/src/config/error.ts +6 -8
- package/src/config/index.ts +16 -5
- package/src/config/keystore/error.ts +9 -0
- package/src/config/keystore/file.ts +96 -0
- package/src/config/keystore/index.ts +4 -0
- package/src/config/keystore/keyring-node.test.ts +12 -0
- package/src/config/keystore/keyring-node.ts +100 -0
- package/src/config/keystore/service.test-utils.ts +15 -0
- package/src/config/keystore/service.test.ts +18 -0
- package/src/config/keystore/service.ts +29 -0
- package/src/config/layer.test-utils.ts +45 -0
- package/src/config/layer.test.ts +209 -0
- package/src/config/paths/index.ts +1 -0
- package/src/config/paths/paths.ts +36 -0
- package/src/config/persist/file-mode.ts +21 -0
- package/src/config/persist/migration.test.ts +71 -0
- package/src/config/persist/migration.ts +82 -0
- package/src/config/persist/persist.test.ts +113 -0
- package/src/config/persist/persist.ts +65 -0
- package/src/config/persist/schema.ts +37 -0
- package/src/config/platform.test-utils.ts +20 -0
- package/src/config/resolve/resolve.test-utils.ts +20 -0
- package/src/config/resolve/resolve.test.ts +137 -0
- package/src/config/resolve/resolve.ts +104 -0
- package/src/config/selection/index.ts +1 -0
- package/src/config/selection/resolve.ts +14 -0
- package/src/config/selection/service.ts +29 -0
- package/src/config/temp-home.test-utils.ts +7 -0
- package/src/config/types.ts +49 -0
- package/src/config/url/error.ts +8 -0
- package/src/config/url/index.ts +7 -0
- package/src/config/url/url.test.ts +31 -0
- package/src/connection/error.ts +4 -1
- package/src/connection/index.ts +0 -1
- package/src/connection/layer.ts +5 -2
- package/src/connection/prepared.ts +83 -0
- package/src/contracts/index.ts +1 -1
- package/src/domain/error.ts +20 -1
- package/src/domain/helpers.test.ts +1 -1
- package/src/domain/helpers.ts +1 -1
- package/src/domain/model-config.ts +1 -1
- package/src/domain/thread-activities.test.ts +1 -1
- package/src/domain/thread-activities.ts +1 -1
- package/src/domain/thread-lifecycle.test.ts +1 -1
- package/src/domain/thread-lifecycle.ts +1 -1
- package/src/effect.test-utils.ts +25 -0
- package/src/index.ts +1 -1
- package/src/{connection/node.ts → node/connection.ts} +1 -1
- package/src/node/index.ts +1 -2
- package/src/orchestration/index.ts +0 -1
- package/src/orchestration/layer.ts +3 -2
- package/src/orchestration/service.ts +1 -1
- package/src/rpc/error.ts +10 -17
- package/src/rpc/layer.ts +17 -51
- package/src/rpc/operation.ts +16 -2
- package/src/rpc/service.ts +2 -1
- package/src/rpc/session.ts +168 -0
- package/src/rpc/ws-group.ts +13 -27
- package/src/runtime/index.ts +2 -1
- package/src/runtime/layer.test-utils.ts +32 -0
- package/src/runtime/layer.test.ts +66 -0
- package/src/runtime/layer.ts +21 -12
- package/src/t3tools/index.ts +1 -1
- package/src/types/marked-terminal.d.ts +11 -0
- package/dist/layout.js +0 -2
- package/dist/scope.js +0 -2
- package/dist/src/cli/output-format.d.ts +0 -11
- package/dist/src/config/layer.d.ts +0 -22
- package/dist/src/config/service.d.ts +0 -22
- package/dist/src/environment/layer.d.ts +0 -3
- package/dist/src/environment/service.d.ts +0 -12
- package/dist/src/layout/index.d.ts +0 -1
- package/dist/upstream-t3code/packages/contracts/src/auth.d.ts +0 -441
- package/dist/upstream-t3code/packages/contracts/src/baseSchemas.d.ts +0 -38
- package/dist/upstream-t3code/packages/contracts/src/desktopBootstrap.d.ts +0 -14
- package/dist/upstream-t3code/packages/contracts/src/editor.d.ts +0 -124
- package/dist/upstream-t3code/packages/contracts/src/environment.d.ts +0 -64
- package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +0 -762
- package/dist/upstream-t3code/packages/contracts/src/filesystem.d.ts +0 -26
- package/dist/upstream-t3code/packages/contracts/src/git.d.ts +0 -500
- package/dist/upstream-t3code/packages/contracts/src/index.d.ts +0 -25
- package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +0 -490
- package/dist/upstream-t3code/packages/contracts/src/keybindings.d.ts +0 -133
- package/dist/upstream-t3code/packages/contracts/src/model.d.ts +0 -112
- package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +0 -6682
- package/dist/upstream-t3code/packages/contracts/src/project.d.ts +0 -45
- package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +0 -116
- package/dist/upstream-t3code/packages/contracts/src/providerInstance.d.ts +0 -99
- package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +0 -4276
- package/dist/upstream-t3code/packages/contracts/src/relay.d.ts +0 -1262
- package/dist/upstream-t3code/packages/contracts/src/relayClient.d.ts +0 -48
- package/dist/upstream-t3code/packages/contracts/src/remoteAccess.d.ts +0 -45
- package/dist/upstream-t3code/packages/contracts/src/review.d.ts +0 -37
- package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +0 -8218
- package/dist/upstream-t3code/packages/contracts/src/server.d.ts +0 -2291
- package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +0 -271
- package/dist/upstream-t3code/packages/contracts/src/sourceControl.d.ts +0 -177
- package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +0 -330
- package/dist/upstream-t3code/packages/contracts/src/vcs.d.ts +0 -148
- package/src/cli/auth-format.ts +0 -45
- package/src/cli/confirm.ts +0 -31
- package/src/cli/model-format.ts +0 -18
- package/src/cli/output-format.ts +0 -45
- package/src/cli/project-format.ts +0 -18
- package/src/cli/terminal/commands.test.ts +0 -101
- package/src/cli/terminal-format.ts +0 -167
- package/src/cli/thread-format.ts +0 -128
- package/src/config/layer.ts +0 -105
- package/src/config/service.ts +0 -26
- package/src/environment/layer.ts +0 -12
- package/src/environment/service.ts +0 -13
- package/src/layout/index.ts +0 -1
- /package/dist/src/{scope → cli/scope}/index.d.ts +0 -0
- /package/dist/src/config/{url.d.ts → url/url.d.ts} +0 -0
- /package/src/{scope → cli/scope}/index.ts +0 -0
- /package/src/config/{url.ts → url/url.ts} +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import * as Cause from "effect/Cause";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Exit from "effect/Exit";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import { assert, describe, it } from "@effect/vitest";
|
|
8
|
+
|
|
9
|
+
import { ConfigError } from "../error.ts";
|
|
10
|
+
import { T3CredentialCrypto } from "../credential/service.ts";
|
|
11
|
+
import { ConfigPlatformLayer } from "../platform.test-utils.ts";
|
|
12
|
+
import { makeTempHomeScoped } from "../temp-home.test-utils.ts";
|
|
13
|
+
import { t3CredentialCryptoDepsLayer } from "../credential/service.test-utils.ts";
|
|
14
|
+
import { migrateV1FileToEncrypted } from "./migration.ts";
|
|
15
|
+
|
|
16
|
+
describe("migrateV1FileToEncrypted", () => {
|
|
17
|
+
it.effect("migrates v1 flat config and roundtrips encrypted tokens", () =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
return yield* makeTempHomeScoped("t3cli-migration-");
|
|
20
|
+
}).pipe(
|
|
21
|
+
Effect.flatMap((homeDir) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const migrated = yield* migrateV1FileToEncrypted({
|
|
24
|
+
url: "https://app.example.com",
|
|
25
|
+
token: "secret-token",
|
|
26
|
+
local: false,
|
|
27
|
+
});
|
|
28
|
+
assert.equal(migrated.default, "app.example.com");
|
|
29
|
+
const crypto = yield* T3CredentialCrypto;
|
|
30
|
+
const token = yield* crypto.decrypt({
|
|
31
|
+
environmentName: "app.example.com",
|
|
32
|
+
url: "https://app.example.com",
|
|
33
|
+
local: false,
|
|
34
|
+
token: migrated.environments["app.example.com"]!.token,
|
|
35
|
+
});
|
|
36
|
+
assert.equal(token, "secret-token");
|
|
37
|
+
}).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir))),
|
|
38
|
+
),
|
|
39
|
+
Effect.provide(ConfigPlatformLayer),
|
|
40
|
+
Effect.scoped,
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
it.effect("maps invalid v1 urls to ConfigError when migrating directly", () =>
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
return yield* makeTempHomeScoped("t3cli-migrate-url-error-");
|
|
47
|
+
}).pipe(
|
|
48
|
+
Effect.flatMap((homeDir) =>
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const exit = yield* migrateV1FileToEncrypted({
|
|
51
|
+
url: "not-a-url",
|
|
52
|
+
token: "secret-token",
|
|
53
|
+
local: false,
|
|
54
|
+
}).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)), Effect.exit);
|
|
55
|
+
assert.isTrue(Exit.isFailure(exit));
|
|
56
|
+
if (!Exit.isFailure(exit)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const error = Cause.findErrorOption(exit.cause);
|
|
60
|
+
assert.isTrue(Option.isSome(error));
|
|
61
|
+
if (Option.isSome(error)) {
|
|
62
|
+
assert.instanceOf(error.value, ConfigError);
|
|
63
|
+
assert.equal(error.value.message, "invalid url");
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
Effect.provide(ConfigPlatformLayer),
|
|
68
|
+
Effect.scoped,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
|
|
5
|
+
import { T3CredentialCrypto } from "../credential/service.ts";
|
|
6
|
+
import { migrateV1EnvironmentName, validateEnvironmentName } from "../environment-name/name.ts";
|
|
7
|
+
import { ConfigError } from "../error.ts";
|
|
8
|
+
import {
|
|
9
|
+
StoredConfigV1FileSchema,
|
|
10
|
+
StoredConfigV2FileSchema,
|
|
11
|
+
type StoredConfigV1File,
|
|
12
|
+
} from "./schema.ts";
|
|
13
|
+
import type { EncryptedConfig } from "../types.ts";
|
|
14
|
+
import { normalizeHttpBaseUrl } from "../url/url.ts";
|
|
15
|
+
|
|
16
|
+
export const emptyEncryptedConfig = (): EncryptedConfig => ({
|
|
17
|
+
version: 2,
|
|
18
|
+
environments: {},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const readEncryptedConfigFromValue = Effect.fn("readEncryptedConfigFromValue")(function* (
|
|
22
|
+
value: unknown,
|
|
23
|
+
) {
|
|
24
|
+
return yield* Effect.gen(function* () {
|
|
25
|
+
const v2 = yield* Schema.decodeUnknownEffect(StoredConfigV2FileSchema)(value).pipe(
|
|
26
|
+
Effect.option,
|
|
27
|
+
);
|
|
28
|
+
if (Option.isSome(v2)) {
|
|
29
|
+
return { config: v2.value, migratedFromV1: false as const };
|
|
30
|
+
}
|
|
31
|
+
const v1 = yield* Schema.decodeUnknownEffect(StoredConfigV1FileSchema)(value);
|
|
32
|
+
const migrated = yield* migrateV1FileToEncrypted(v1);
|
|
33
|
+
return { config: migrated, migratedFromV1: true as const };
|
|
34
|
+
}).pipe(
|
|
35
|
+
Effect.catchTags({
|
|
36
|
+
SchemaError: (error) =>
|
|
37
|
+
Effect.fail(new ConfigError({ message: "failed to read config", cause: error })),
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const migrateV1FileToEncrypted = Effect.fn("migrateV1FileToEncrypted")(function* (
|
|
43
|
+
config: StoredConfigV1File,
|
|
44
|
+
) {
|
|
45
|
+
if (
|
|
46
|
+
(config.url === undefined || config.url.length === 0) &&
|
|
47
|
+
(config.token === undefined || config.token.length === 0)
|
|
48
|
+
) {
|
|
49
|
+
return emptyEncryptedConfig();
|
|
50
|
+
}
|
|
51
|
+
if (config.url === undefined || config.token === undefined) {
|
|
52
|
+
return yield* Effect.fail(
|
|
53
|
+
new ConfigError({ message: "failed to read config: incomplete v1 credentials" }),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const name = yield* migrateV1EnvironmentName(config).pipe(
|
|
57
|
+
Effect.flatMap((migratedName) =>
|
|
58
|
+
validateEnvironmentName(migratedName).pipe(Effect.as(migratedName)),
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
const normalizedUrl = yield* normalizeHttpBaseUrl(config.url).pipe(
|
|
62
|
+
Effect.mapError((error) => new ConfigError({ message: error.message, cause: error.cause })),
|
|
63
|
+
);
|
|
64
|
+
const crypto = yield* T3CredentialCrypto;
|
|
65
|
+
const token = yield* crypto.encrypt({
|
|
66
|
+
environmentName: name,
|
|
67
|
+
url: normalizedUrl,
|
|
68
|
+
local: config.local ?? false,
|
|
69
|
+
token: config.token,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
version: 2 as const,
|
|
73
|
+
default: name,
|
|
74
|
+
environments: {
|
|
75
|
+
[name]: {
|
|
76
|
+
url: normalizedUrl,
|
|
77
|
+
local: config.local ?? false,
|
|
78
|
+
token,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
} satisfies EncryptedConfig;
|
|
82
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as FileSystem from "effect/FileSystem";
|
|
5
|
+
import * as Path from "effect/Path";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
import { assert, describe, it } from "@effect/vitest";
|
|
8
|
+
|
|
9
|
+
import { T3Config } from "../config.ts";
|
|
10
|
+
import { ConfigPlatformLayer } from "../platform.test-utils.ts";
|
|
11
|
+
import { makeTempHomeScoped } from "../temp-home.test-utils.ts";
|
|
12
|
+
import { t3ConfigDepsLayer } from "../layer.test-utils.ts";
|
|
13
|
+
import { StoredConfigV1FileJson, StoredConfigV2FileJson } from "./schema.ts";
|
|
14
|
+
|
|
15
|
+
describe("config file persistence", () => {
|
|
16
|
+
it.effect("persists v1 config as encrypted v2 on first read", () =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
return yield* makeTempHomeScoped("t3cli-persist-");
|
|
19
|
+
}).pipe(
|
|
20
|
+
Effect.flatMap((homeDir) =>
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23
|
+
const path = yield* Path.Path;
|
|
24
|
+
const secretToken = "legacy-plaintext-token";
|
|
25
|
+
const configPath = path.join(homeDir, ".config", "t3cli", "config.json");
|
|
26
|
+
yield* fs.makeDirectory(path.dirname(configPath), { recursive: true });
|
|
27
|
+
const legacyConfig = yield* Schema.encodeEffect(StoredConfigV1FileJson)({
|
|
28
|
+
url: "https://home.example",
|
|
29
|
+
token: secretToken,
|
|
30
|
+
local: false,
|
|
31
|
+
});
|
|
32
|
+
yield* fs.writeFileString(configPath, `${legacyConfig}\n`, { mode: 0o600 });
|
|
33
|
+
yield* Effect.gen(function* () {
|
|
34
|
+
const config = yield* T3Config;
|
|
35
|
+
yield* config.listEnvironments();
|
|
36
|
+
}).pipe(Effect.provide(t3ConfigDepsLayer(homeDir)));
|
|
37
|
+
const raw = yield* fs.readFileString(configPath);
|
|
38
|
+
assert.equal(raw.includes(secretToken), false);
|
|
39
|
+
const persisted = yield* Schema.decodeUnknownEffect(StoredConfigV2FileJson)(raw);
|
|
40
|
+
assert.equal(persisted.version, 2);
|
|
41
|
+
const environment = Object.values(persisted.environments)[0];
|
|
42
|
+
assert.isDefined(environment);
|
|
43
|
+
assert.equal(environment.token.kind, "encrypted");
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
Effect.provide(ConfigPlatformLayer),
|
|
47
|
+
Effect.scoped,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
it.effect("never writes plaintext tokens to config.json", () =>
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
return yield* makeTempHomeScoped("t3cli-persist-");
|
|
54
|
+
}).pipe(
|
|
55
|
+
Effect.flatMap((homeDir) =>
|
|
56
|
+
Effect.gen(function* () {
|
|
57
|
+
const fs = yield* FileSystem.FileSystem;
|
|
58
|
+
const path = yield* Path.Path;
|
|
59
|
+
const secretToken = "super-secret-token-value";
|
|
60
|
+
yield* Effect.gen(function* () {
|
|
61
|
+
const config = yield* T3Config;
|
|
62
|
+
yield* config.upsertEnvironment({
|
|
63
|
+
name: "home",
|
|
64
|
+
url: "https://home.example",
|
|
65
|
+
token: secretToken,
|
|
66
|
+
local: false,
|
|
67
|
+
});
|
|
68
|
+
}).pipe(Effect.provide(t3ConfigDepsLayer(homeDir)));
|
|
69
|
+
const configPath = path.join(homeDir, ".config", "t3cli", "config.json");
|
|
70
|
+
const raw = yield* fs.readFileString(configPath);
|
|
71
|
+
assert.equal(raw.includes(secretToken), false);
|
|
72
|
+
const persisted = yield* Schema.decodeUnknownEffect(StoredConfigV2FileJson)(raw);
|
|
73
|
+
assert.equal(persisted.environments.home?.token.kind, "encrypted");
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
Effect.provide(ConfigPlatformLayer),
|
|
77
|
+
Effect.scoped,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
it.effect("hardens existing config file permissions to 0600 on write", () =>
|
|
82
|
+
Effect.gen(function* () {
|
|
83
|
+
return yield* makeTempHomeScoped("t3cli-persist-");
|
|
84
|
+
}).pipe(
|
|
85
|
+
Effect.flatMap((homeDir) =>
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
const fs = yield* FileSystem.FileSystem;
|
|
88
|
+
const path = yield* Path.Path;
|
|
89
|
+
const configPath = path.join(homeDir, ".config", "t3cli", "config.json");
|
|
90
|
+
yield* fs.makeDirectory(path.dirname(configPath), { recursive: true });
|
|
91
|
+
const emptyConfig = yield* Schema.encodeEffect(StoredConfigV2FileJson)({
|
|
92
|
+
version: 2,
|
|
93
|
+
environments: {},
|
|
94
|
+
});
|
|
95
|
+
yield* fs.writeFileString(configPath, `${emptyConfig}\n`, { mode: 0o644 });
|
|
96
|
+
yield* Effect.gen(function* () {
|
|
97
|
+
const config = yield* T3Config;
|
|
98
|
+
yield* config.upsertEnvironment({
|
|
99
|
+
name: "home",
|
|
100
|
+
url: "https://home.example",
|
|
101
|
+
token: "home-token",
|
|
102
|
+
local: false,
|
|
103
|
+
});
|
|
104
|
+
}).pipe(Effect.provide(t3ConfigDepsLayer(homeDir)));
|
|
105
|
+
const configStat = yield* fs.stat(configPath);
|
|
106
|
+
assert.equal(configStat.mode & 0o777, 0o600);
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
Effect.provide(ConfigPlatformLayer),
|
|
110
|
+
Effect.scoped,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as FileSystem from "effect/FileSystem";
|
|
3
|
+
import * as Path from "effect/Path";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
|
|
6
|
+
import { ConfigError, isPlatformNotFoundError } from "../error.ts";
|
|
7
|
+
import { hardenPrivateFileMode } from "./file-mode.ts";
|
|
8
|
+
import { emptyEncryptedConfig, readEncryptedConfigFromValue } from "./migration.ts";
|
|
9
|
+
import { resolveConfigFilePath } from "../paths/paths.ts";
|
|
10
|
+
import { UnknownConfigFileJson, StoredConfigV2FileJson } from "./schema.ts";
|
|
11
|
+
import type { EncryptedConfig } from "../types.ts";
|
|
12
|
+
|
|
13
|
+
export const readEncryptedConfigFile = Effect.fn("readEncryptedConfigFile")(function* () {
|
|
14
|
+
const fs = yield* FileSystem.FileSystem;
|
|
15
|
+
const configFilePath = yield* resolveConfigFilePath();
|
|
16
|
+
const raw = yield* fs.readFileString(configFilePath).pipe(
|
|
17
|
+
Effect.catchTags({
|
|
18
|
+
PlatformError: (error) =>
|
|
19
|
+
isPlatformNotFoundError(error)
|
|
20
|
+
? Effect.succeed(undefined)
|
|
21
|
+
: Effect.fail(new ConfigError({ message: "failed to read config", cause: error })),
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
if (raw === undefined) {
|
|
25
|
+
return emptyEncryptedConfig();
|
|
26
|
+
}
|
|
27
|
+
const value = yield* Schema.decodeUnknownEffect(UnknownConfigFileJson)(raw).pipe(
|
|
28
|
+
Effect.catchTags({
|
|
29
|
+
SchemaError: (error) =>
|
|
30
|
+
Effect.fail(new ConfigError({ message: "failed to read config", cause: error })),
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
const read = yield* readEncryptedConfigFromValue(value);
|
|
34
|
+
if (read.migratedFromV1) {
|
|
35
|
+
yield* writeEncryptedConfigFile(read.config);
|
|
36
|
+
}
|
|
37
|
+
return read.config;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const writeEncryptedConfigFile = Effect.fn("writeEncryptedConfigFile")(function* (
|
|
41
|
+
config: EncryptedConfig,
|
|
42
|
+
) {
|
|
43
|
+
const fs = yield* FileSystem.FileSystem;
|
|
44
|
+
const path = yield* Path.Path;
|
|
45
|
+
const configFilePath = yield* resolveConfigFilePath();
|
|
46
|
+
yield* fs.makeDirectory(path.dirname(configFilePath), { recursive: true, mode: 0o700 }).pipe(
|
|
47
|
+
Effect.catchTags({
|
|
48
|
+
PlatformError: (error) =>
|
|
49
|
+
Effect.fail(new ConfigError({ message: "failed to write config", cause: error })),
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
const encoded = yield* Schema.encodeEffect(StoredConfigV2FileJson)(config).pipe(
|
|
53
|
+
Effect.catchTags({
|
|
54
|
+
SchemaError: (error) =>
|
|
55
|
+
Effect.fail(new ConfigError({ message: "failed to write config", cause: error })),
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
yield* fs.writeFileString(configFilePath, `${encoded}\n`, { mode: 0o600 }).pipe(
|
|
59
|
+
Effect.catchTags({
|
|
60
|
+
PlatformError: (error) =>
|
|
61
|
+
Effect.fail(new ConfigError({ message: "failed to write config", cause: error })),
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
yield* hardenPrivateFileMode(configFilePath, "config");
|
|
65
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
export const EncryptedTokenSchema = Schema.Struct({
|
|
4
|
+
kind: Schema.Literal("encrypted"),
|
|
5
|
+
alg: Schema.Literal("aes-256-gcm"),
|
|
6
|
+
key: Schema.Literal("default"),
|
|
7
|
+
nonce: Schema.String,
|
|
8
|
+
ciphertext: Schema.String,
|
|
9
|
+
tag: Schema.String,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const StoredEnvironmentFileSchema = Schema.Struct({
|
|
13
|
+
url: Schema.String,
|
|
14
|
+
local: Schema.Boolean,
|
|
15
|
+
token: EncryptedTokenSchema,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const StoredConfigV2FileSchema = Schema.Struct({
|
|
19
|
+
version: Schema.Literal(2),
|
|
20
|
+
default: Schema.optionalKey(Schema.String),
|
|
21
|
+
environments: Schema.Record(Schema.String, StoredEnvironmentFileSchema),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const StoredConfigV1FileSchema = Schema.Struct({
|
|
25
|
+
url: Schema.optionalKey(Schema.String),
|
|
26
|
+
token: Schema.optionalKey(Schema.String),
|
|
27
|
+
local: Schema.optionalKey(Schema.Boolean),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type EncryptedToken = Schema.Schema.Type<typeof EncryptedTokenSchema>;
|
|
31
|
+
export type StoredEnvironmentFile = Schema.Schema.Type<typeof StoredEnvironmentFileSchema>;
|
|
32
|
+
export type StoredConfigV2File = Schema.Schema.Type<typeof StoredConfigV2FileSchema>;
|
|
33
|
+
export type StoredConfigV1File = Schema.Schema.Type<typeof StoredConfigV1FileSchema>;
|
|
34
|
+
|
|
35
|
+
export const StoredConfigV1FileJson = Schema.fromJsonString(StoredConfigV1FileSchema);
|
|
36
|
+
export const StoredConfigV2FileJson = Schema.fromJsonString(StoredConfigV2FileSchema);
|
|
37
|
+
export const UnknownConfigFileJson = Schema.UnknownFromJsonString;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Layer from "effect/Layer";
|
|
2
|
+
import * as NodeChildProcessSpawner from "@effect/platform-node/NodeChildProcessSpawner";
|
|
3
|
+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
|
|
4
|
+
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
|
|
5
|
+
import * as NodePath from "@effect/platform-node/NodePath";
|
|
6
|
+
import * as NodeStdio from "@effect/platform-node/NodeStdio";
|
|
7
|
+
import * as NodeTerminal from "@effect/platform-node/NodeTerminal";
|
|
8
|
+
|
|
9
|
+
export const ConfigPlatformLayer = Layer.mergeAll(
|
|
10
|
+
NodeFileSystem.layer,
|
|
11
|
+
NodePath.layer,
|
|
12
|
+
NodeCrypto.layer,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const CliCommandPlatformLayer = Layer.mergeAll(
|
|
16
|
+
ConfigPlatformLayer,
|
|
17
|
+
NodeStdio.layer,
|
|
18
|
+
NodeTerminal.layer,
|
|
19
|
+
NodeChildProcessSpawner.layer,
|
|
20
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EncryptedConfig } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export const sampleEncrypted = (input: {
|
|
4
|
+
readonly default?: string;
|
|
5
|
+
readonly environments?: EncryptedConfig["environments"];
|
|
6
|
+
}): EncryptedConfig => ({
|
|
7
|
+
version: 2,
|
|
8
|
+
...(input.default !== undefined ? { default: input.default } : {}),
|
|
9
|
+
environments: input.environments ?? {},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const sampleEncryptedToken = () =>
|
|
13
|
+
({
|
|
14
|
+
kind: "encrypted" as const,
|
|
15
|
+
alg: "aes-256-gcm" as const,
|
|
16
|
+
key: "default" as const,
|
|
17
|
+
nonce: "n",
|
|
18
|
+
ciphertext: "c",
|
|
19
|
+
tag: "t",
|
|
20
|
+
}) as const;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import { assert, describe, it } from "@effect/vitest";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Exit from "effect/Exit";
|
|
6
|
+
|
|
7
|
+
import { resolveConfiguredEnvironment } from "../selection/resolve.ts";
|
|
8
|
+
import { sampleEncrypted, sampleEncryptedToken } from "./resolve.test-utils.ts";
|
|
9
|
+
import {
|
|
10
|
+
buildResolvedConfigFromEnv,
|
|
11
|
+
resolveDefaultForUpsert,
|
|
12
|
+
selectEnvironmentName,
|
|
13
|
+
validateCredentialEnvVars,
|
|
14
|
+
} from "./resolve.ts";
|
|
15
|
+
|
|
16
|
+
describe("resolveConfiguredEnvironment", () => {
|
|
17
|
+
it("prefers cli flag over T3CLI_ENV", () => {
|
|
18
|
+
assert.equal(
|
|
19
|
+
resolveConfiguredEnvironment({
|
|
20
|
+
cliFlag: "cli",
|
|
21
|
+
t3cliEnv: "env",
|
|
22
|
+
}),
|
|
23
|
+
"cli",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("falls back to T3CLI_ENV when cli flag is absent", () => {
|
|
28
|
+
assert.equal(
|
|
29
|
+
resolveConfiguredEnvironment({
|
|
30
|
+
t3cliEnv: "env",
|
|
31
|
+
}),
|
|
32
|
+
"env",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("selectEnvironmentName", () => {
|
|
38
|
+
it("prefers selected environment over config default", () => {
|
|
39
|
+
const selected = selectEnvironmentName({
|
|
40
|
+
selectedEnvironment: "cli",
|
|
41
|
+
defaultEnvironment: "default",
|
|
42
|
+
});
|
|
43
|
+
assert.equal(selected, "cli");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("falls back to config default", () => {
|
|
47
|
+
assert.equal(
|
|
48
|
+
selectEnvironmentName({
|
|
49
|
+
defaultEnvironment: "default",
|
|
50
|
+
}),
|
|
51
|
+
"default",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("validateCredentialEnvVars", () => {
|
|
57
|
+
it.effect("requires both T3CODE_URL and T3CODE_TOKEN together", () =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const onlyUrl = yield* validateCredentialEnvVars({ envUrl: "https://example.com" }).pipe(
|
|
60
|
+
Effect.exit,
|
|
61
|
+
);
|
|
62
|
+
assert.equal(Exit.isFailure(onlyUrl), true);
|
|
63
|
+
|
|
64
|
+
const onlyToken = yield* validateCredentialEnvVars({ envToken: "token" }).pipe(Effect.exit);
|
|
65
|
+
assert.equal(Exit.isFailure(onlyToken), true);
|
|
66
|
+
|
|
67
|
+
yield* validateCredentialEnvVars({
|
|
68
|
+
envUrl: "https://example.com",
|
|
69
|
+
envToken: "token",
|
|
70
|
+
});
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("buildResolvedConfigFromEnv", () => {
|
|
76
|
+
it.effect(
|
|
77
|
+
"does not inherit local=true from selected stored environment when env vars override",
|
|
78
|
+
() =>
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
const resolved = yield* buildResolvedConfigFromEnv({
|
|
81
|
+
envUrl: "https://remote.example",
|
|
82
|
+
envToken: "env-token",
|
|
83
|
+
});
|
|
84
|
+
assert.equal(resolved.source, "env");
|
|
85
|
+
assert.equal(resolved.local, false);
|
|
86
|
+
assert.equal(resolved.url, "https://remote.example");
|
|
87
|
+
assert.equal(resolved.token, "env-token");
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("resolveDefaultForUpsert", () => {
|
|
93
|
+
it("sets default only for the first environment", () => {
|
|
94
|
+
assert.equal(resolveDefaultForUpsert(sampleEncrypted({}), "first"), "first");
|
|
95
|
+
assert.equal(
|
|
96
|
+
resolveDefaultForUpsert(
|
|
97
|
+
sampleEncrypted({
|
|
98
|
+
default: "home",
|
|
99
|
+
environments: {
|
|
100
|
+
home: {
|
|
101
|
+
url: "https://home.example",
|
|
102
|
+
local: false,
|
|
103
|
+
token: sampleEncryptedToken(),
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
"work",
|
|
108
|
+
),
|
|
109
|
+
"home",
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("promotes default when makeDefault is true", () => {
|
|
114
|
+
assert.equal(
|
|
115
|
+
resolveDefaultForUpsert(
|
|
116
|
+
sampleEncrypted({
|
|
117
|
+
default: "home",
|
|
118
|
+
environments: {
|
|
119
|
+
home: {
|
|
120
|
+
url: "https://home.example",
|
|
121
|
+
local: false,
|
|
122
|
+
token: sampleEncryptedToken(),
|
|
123
|
+
},
|
|
124
|
+
work: {
|
|
125
|
+
url: "https://work.example",
|
|
126
|
+
local: false,
|
|
127
|
+
token: { ...sampleEncryptedToken(), nonce: "n2", ciphertext: "c2", tag: "t2" },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
"work",
|
|
132
|
+
true,
|
|
133
|
+
),
|
|
134
|
+
"work",
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
|
|
3
|
+
import { validateEnvironmentName } from "../environment-name/name.ts";
|
|
4
|
+
import { ConfigError } from "../error.ts";
|
|
5
|
+
import type { EncryptedConfig, ResolvedConfig } from "../types.ts";
|
|
6
|
+
import { normalizeHttpBaseUrl } from "../url/url.ts";
|
|
7
|
+
|
|
8
|
+
export type ResolveSelectionInput = {
|
|
9
|
+
readonly selectedEnvironment?: string | undefined;
|
|
10
|
+
readonly defaultEnvironment?: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function selectEnvironmentName(input: ResolveSelectionInput) {
|
|
14
|
+
if (input.selectedEnvironment !== undefined && input.selectedEnvironment.length > 0) {
|
|
15
|
+
return input.selectedEnvironment;
|
|
16
|
+
}
|
|
17
|
+
return input.defaultEnvironment;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ResolveCredentialInput = {
|
|
21
|
+
readonly envUrl?: string | undefined;
|
|
22
|
+
readonly envToken?: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function validateCredentialEnvVars(input: ResolveCredentialInput) {
|
|
26
|
+
const hasEnvUrl = input.envUrl !== undefined && input.envUrl.length > 0;
|
|
27
|
+
const hasEnvToken = input.envToken !== undefined && input.envToken.length > 0;
|
|
28
|
+
if (hasEnvUrl !== hasEnvToken) {
|
|
29
|
+
return Effect.fail(
|
|
30
|
+
new ConfigError({
|
|
31
|
+
message: "T3CODE_URL and T3CODE_TOKEN must both be set together",
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return Effect.void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveDefaultForUpsert(
|
|
39
|
+
encrypted: EncryptedConfig,
|
|
40
|
+
environmentName: string,
|
|
41
|
+
makeDefault?: boolean,
|
|
42
|
+
): string | undefined {
|
|
43
|
+
if (makeDefault === true) {
|
|
44
|
+
return environmentName;
|
|
45
|
+
}
|
|
46
|
+
if (Object.keys(encrypted.environments).length === 0) {
|
|
47
|
+
return environmentName;
|
|
48
|
+
}
|
|
49
|
+
return encrypted.default;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function summarizeEnvironments(encrypted: EncryptedConfig) {
|
|
53
|
+
return Object.entries(encrypted.environments)
|
|
54
|
+
.map(([name, environmentConfig]) => ({
|
|
55
|
+
name,
|
|
56
|
+
url: environmentConfig.url,
|
|
57
|
+
local: environmentConfig.local,
|
|
58
|
+
default: encrypted.default === name,
|
|
59
|
+
}))
|
|
60
|
+
.toSorted((left, right) => left.name.localeCompare(right.name));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildResolvedConfigFromEnv(input: {
|
|
64
|
+
readonly envUrl: string;
|
|
65
|
+
readonly envToken: string;
|
|
66
|
+
}) {
|
|
67
|
+
return normalizeHttpBaseUrl(input.envUrl).pipe(
|
|
68
|
+
Effect.mapError((error) => new ConfigError({ message: error.message, cause: error.cause })),
|
|
69
|
+
Effect.map((normalizedUrl) => {
|
|
70
|
+
return {
|
|
71
|
+
url: normalizedUrl,
|
|
72
|
+
token: input.envToken,
|
|
73
|
+
source: "env" as const,
|
|
74
|
+
local: false,
|
|
75
|
+
} satisfies ResolvedConfig;
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildResolvedConfigFromStored(input: {
|
|
81
|
+
readonly selectedName: string;
|
|
82
|
+
readonly token: string;
|
|
83
|
+
readonly encrypted: EncryptedConfig;
|
|
84
|
+
}) {
|
|
85
|
+
return Effect.gen(function* () {
|
|
86
|
+
yield* validateEnvironmentName(input.selectedName);
|
|
87
|
+
const selectedEnvironment = input.encrypted.environments[input.selectedName];
|
|
88
|
+
if (selectedEnvironment === undefined) {
|
|
89
|
+
return yield* Effect.fail(
|
|
90
|
+
new ConfigError({ message: `environment not found: ${input.selectedName}` }),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const normalizedUrl = yield* normalizeHttpBaseUrl(selectedEnvironment.url).pipe(
|
|
94
|
+
Effect.mapError((error) => new ConfigError({ message: error.message, cause: error.cause })),
|
|
95
|
+
);
|
|
96
|
+
return {
|
|
97
|
+
url: normalizedUrl,
|
|
98
|
+
token: input.token,
|
|
99
|
+
source: "config" as const,
|
|
100
|
+
local: selectedEnvironment.local,
|
|
101
|
+
environment: input.selectedName,
|
|
102
|
+
} satisfies ResolvedConfig;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Service from "./service.ts";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function resolveConfiguredEnvironment(input: {
|
|
2
|
+
readonly cliFlag?: string | undefined;
|
|
3
|
+
readonly t3cliEnv?: string | undefined;
|
|
4
|
+
}): string | undefined {
|
|
5
|
+
const fromFlag = input.cliFlag?.trim();
|
|
6
|
+
if (fromFlag !== undefined && fromFlag.length > 0) {
|
|
7
|
+
return fromFlag;
|
|
8
|
+
}
|
|
9
|
+
const fromEnv = input.t3cliEnv?.trim();
|
|
10
|
+
if (fromEnv !== undefined && fromEnv.length > 0) {
|
|
11
|
+
return fromEnv;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|