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.
- package/README.md +42 -10
- package/dist/application.js +2 -2
- package/dist/auth.js +1 -1
- package/dist/bin.js +85526 -2662
- 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/rpc.js +1 -1
- package/dist/runtime.js +2 -2
- package/dist/shared.js +42956 -38119
- 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 +51 -35
- 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 +22 -2
- 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 +11 -0
- package/dist/src/application/terminals.d.ts +2 -2
- package/dist/src/application/thread-commands.d.ts +1 -1
- package/dist/src/application/thread-wait.d.ts +9 -0
- package/dist/src/application/threads.d.ts +56 -5
- package/dist/src/auth/error.d.ts +2 -1
- package/dist/src/auth/index.d.ts +1 -1
- package/dist/src/auth/layer.d.ts +34 -6
- 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/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/index.d.ts +0 -1
- 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 +16 -1
- package/dist/src/domain/model-config.d.ts +4 -1
- package/dist/src/domain/thread-activities.d.ts +1 -1
- package/dist/src/domain/thread-lifecycle.d.ts +10 -1
- 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 +153 -7
- package/dist/src/orchestration/service.d.ts +1 -1
- package/dist/src/rpc/error.d.ts +2 -2
- package/dist/src/rpc/index.d.ts +2 -3
- package/dist/src/rpc/layer.d.ts +574 -46
- package/dist/src/rpc/operation.d.ts +574 -46
- package/dist/src/rpc/ws-group.d.ts +184 -19
- package/dist/src/runtime/index.d.ts +1 -2
- package/dist/src/runtime/layer.d.ts +12 -8
- package/dist/src/t3tools/index.d.ts +1 -1
- package/dist/t3tools.js +2 -2
- package/package.json +25 -21
- 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 +1 -1
- package/src/application/threads.test.ts +9 -3
- package/src/application/threads.ts +6 -6
- package/src/auth/error.ts +2 -1
- 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/service.test.ts +200 -0
- package/src/auth/service.ts +27 -3
- package/src/auth/transport.ts +2 -2
- 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/index.ts +0 -1
- 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 +1 -1
- package/src/orchestration/service.ts +1 -1
- package/src/rpc/error.ts +2 -2
- package/src/rpc/index.ts +2 -3
- package/src/rpc/layer.ts +1 -1
- package/src/rpc/ws-group.ts +1 -1
- 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 +15 -9
- 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,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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Option from "effect/Option";
|
|
5
|
+
|
|
6
|
+
import { loadT3CliEnv } from "../env/env.ts";
|
|
7
|
+
import { resolveConfiguredEnvironment } from "./resolve.ts";
|
|
8
|
+
|
|
9
|
+
export class T3ConfigSelection extends Context.Service<
|
|
10
|
+
T3ConfigSelection,
|
|
11
|
+
{
|
|
12
|
+
readonly getSelectedEnvironment: () => Effect.Effect<string | undefined>;
|
|
13
|
+
}
|
|
14
|
+
>()("t3cli/T3ConfigSelection") {}
|
|
15
|
+
|
|
16
|
+
export const layer = Layer.effect(
|
|
17
|
+
T3ConfigSelection,
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const t3CliEnv = yield* loadT3CliEnv;
|
|
20
|
+
return {
|
|
21
|
+
getSelectedEnvironment: () =>
|
|
22
|
+
Effect.sync(() =>
|
|
23
|
+
resolveConfiguredEnvironment({
|
|
24
|
+
t3cliEnv: Option.getOrUndefined(t3CliEnv.t3cliEnv),
|
|
25
|
+
}),
|
|
26
|
+
),
|
|
27
|
+
};
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as FileSystem from "effect/FileSystem";
|
|
3
|
+
|
|
4
|
+
export const makeTempHomeScoped = Effect.fn("makeTempHomeScoped")(function* (prefix: string) {
|
|
5
|
+
const fs = yield* FileSystem.FileSystem;
|
|
6
|
+
return yield* fs.makeTempDirectoryScoped({ prefix });
|
|
7
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EncryptedToken,
|
|
3
|
+
StoredConfigV2File,
|
|
4
|
+
StoredEnvironmentFile,
|
|
5
|
+
} from "./persist/schema.ts";
|
|
6
|
+
|
|
7
|
+
export type EncryptedEnvironment = StoredEnvironmentFile;
|
|
8
|
+
|
|
9
|
+
export type EncryptedConfig = StoredConfigV2File;
|
|
10
|
+
|
|
11
|
+
export type DecryptedEnvironment = {
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly local: boolean;
|
|
14
|
+
readonly token: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DecryptedConfig = {
|
|
18
|
+
readonly version: 2;
|
|
19
|
+
readonly default?: string;
|
|
20
|
+
readonly environments: Readonly<Record<string, DecryptedEnvironment>>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type EnvironmentSummary = {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly url: string;
|
|
26
|
+
readonly local: boolean;
|
|
27
|
+
readonly default: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ResolvedConfig = {
|
|
31
|
+
readonly url: string;
|
|
32
|
+
readonly token: string;
|
|
33
|
+
readonly source: "env" | "config";
|
|
34
|
+
readonly local: boolean;
|
|
35
|
+
readonly environment?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type UpsertEnvironmentInput = {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly url: string;
|
|
41
|
+
readonly token: string;
|
|
42
|
+
readonly local: boolean;
|
|
43
|
+
readonly makeDefault?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type StoredConfig = DecryptedConfig;
|
|
47
|
+
export type StoredEnvironment = DecryptedEnvironment;
|
|
48
|
+
|
|
49
|
+
export type { EncryptedToken };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as Cause from "effect/Cause";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
|
|
4
|
+
export class UrlError extends Schema.TaggedErrorClass<UrlError>()("UrlError", {
|
|
5
|
+
message: Schema.String,
|
|
6
|
+
protocol: Schema.optionalKey(Schema.String),
|
|
7
|
+
cause: Schema.optionalKey(Schema.instanceOf(Cause.IllegalArgumentError)),
|
|
8
|
+
}) {}
|
|
@@ -0,0 +1,31 @@
|
|
|
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 { expectFailWithMessage } from "../../effect.test-utils.ts";
|
|
7
|
+
import { UrlError } from "./error.ts";
|
|
8
|
+
import { normalizeHttpBaseUrl, toWebSocketEndpointUrl } from "./url.ts";
|
|
9
|
+
|
|
10
|
+
describe("normalizeHttpBaseUrl", () => {
|
|
11
|
+
it.effect("strips query and trailing slash from urls with paths", () =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const normalized = yield* normalizeHttpBaseUrl("https://app.example.com/api/?q=1");
|
|
14
|
+
assert.equal(normalized, "https://app.example.com/api");
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
it.effect("maps invalid urls to UrlError", () =>
|
|
19
|
+
expectFailWithMessage(normalizeHttpBaseUrl("not-a-url"), UrlError, "invalid url"),
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("toWebSocketEndpointUrl", () => {
|
|
24
|
+
it.effect("rejects unsupported protocols with UrlError", () =>
|
|
25
|
+
expectFailWithMessage(
|
|
26
|
+
toWebSocketEndpointUrl("ftp://example.com", "/ws"),
|
|
27
|
+
UrlError,
|
|
28
|
+
"unsupported server url protocol: ftp:",
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
});
|
package/src/connection/index.ts
CHANGED
package/src/contracts/index.ts
CHANGED