authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-03 19:49:10-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-03 19:49:10-07:00
log27c2b145f8d214b5c578eefc463278d0916fb048
tree6fc4853e053d67f3898c2f819a1dc42331346128
parent8515f02770a05d5f1c0f740c5223938fb6ab102b
signature Signed by PGP key B5690EEEBB952194

feat(cli): allow optional positional cwd argument (#1727)

Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>

2 files changed, 22 insertions(+), 3 deletions(-)

apps/server/src/cli-config.test.ts+9
...@@ -40,6 +40,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -40,6 +40,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
40 port: Option.none(),40 port: Option.none(),
41 host: Option.none(),41 host: Option.none(),
42 baseDir: Option.none(),42 baseDir: Option.none(),
43 cwd: Option.none(),
43 devUrl: Option.none(),44 devUrl: Option.none(),
44 noBrowser: Option.none(),45 noBrowser: Option.none(),
45 authToken: Option.none(),46 authToken: Option.none(),
...@@ -102,6 +103,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -102,6 +103,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
102 port: Option.some(8788),103 port: Option.some(8788),
103 host: Option.some("127.0.0.1"),104 host: Option.some("127.0.0.1"),
104 baseDir: Option.some(baseDir),105 baseDir: Option.some(baseDir),
106 cwd: Option.none(),
105 devUrl: Option.some(new URL("http://127.0.0.1:4173")),107 devUrl: Option.some(new URL("http://127.0.0.1:4173")),
106 noBrowser: Option.some(true),108 noBrowser: Option.some(true),
107 authToken: Option.some("flag-token"),109 authToken: Option.some("flag-token"),
...@@ -178,6 +180,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -178,6 +180,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
178 port: Option.none(),180 port: Option.none(),
179 host: Option.none(),181 host: Option.none(),
180 baseDir: Option.none(),182 baseDir: Option.none(),
183 cwd: Option.none(),
181 devUrl: Option.none(),184 devUrl: Option.none(),
182 noBrowser: Option.none(),185 noBrowser: Option.none(),
183 authToken: Option.none(),186 authToken: Option.none(),
...@@ -228,6 +231,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -228,6 +231,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
228 const fs = yield* FileSystem.FileSystem;231 const fs = yield* FileSystem.FileSystem;
229 const path = yield* Path.Path;232 const path = yield* Path.Path;
230 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cli-config-dirs-" });233 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cli-config-dirs-" });
234 const customCwd = path.join(baseDir, "nested", "project");
231235
232 const resolved = yield* resolveServerConfig(236 const resolved = yield* resolveServerConfig(
233 {237 {
...@@ -235,6 +239,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -235,6 +239,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
235 port: Option.some(4888),239 port: Option.some(4888),
236 host: Option.none(),240 host: Option.none(),
237 baseDir: Option.some(baseDir),241 baseDir: Option.some(baseDir),
242 cwd: Option.some(customCwd),
238 devUrl: Option.some(new URL("http://127.0.0.1:5173")),243 devUrl: Option.some(new URL("http://127.0.0.1:5173")),
239 noBrowser: Option.none(),244 noBrowser: Option.none(),
240 authToken: Option.none(),245 authToken: Option.none(),
...@@ -253,6 +258,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -253,6 +258,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
253 );258 );
254259
255 for (const directory of [260 for (const directory of [
261 customCwd,
256 resolved.stateDir,262 resolved.stateDir,
257 resolved.logsDir,263 resolved.logsDir,
258 resolved.providerLogsDir,264 resolved.providerLogsDir,
...@@ -264,6 +270,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -264,6 +270,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
264 ]) {270 ]) {
265 expect(yield* fs.exists(directory)).toBe(true);271 expect(yield* fs.exists(directory)).toBe(true);
266 }272 }
273 expect(resolved.cwd).toBe(path.resolve(customCwd));
267 }),274 }),
268 );275 );
269276
...@@ -290,6 +297,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -290,6 +297,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
290 port: Option.some(8788),297 port: Option.some(8788),
291 host: Option.some("127.0.0.1"),298 host: Option.some("127.0.0.1"),
292 baseDir: Option.none(),299 baseDir: Option.none(),
300 cwd: Option.none(),
293 devUrl: Option.some(new URL("http://127.0.0.1:4173")),301 devUrl: Option.some(new URL("http://127.0.0.1:4173")),
294 noBrowser: Option.none(),302 noBrowser: Option.none(),
295 authToken: Option.some("flag-token"),303 authToken: Option.some("flag-token"),
...@@ -360,6 +368,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -360,6 +368,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
360 port: Option.some(4888),368 port: Option.some(4888),
361 host: Option.none(),369 host: Option.none(),
362 baseDir: Option.some(baseDir),370 baseDir: Option.some(baseDir),
371 cwd: Option.none(),
363 devUrl: Option.none(),372 devUrl: Option.none(),
364 noBrowser: Option.none(),373 noBrowser: Option.none(),
365 authToken: Option.none(),374 authToken: Option.none(),
apps/server/src/cli.ts+13-3
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { NetService } from "@t3tools/shared/Net";1import { NetService } from "@t3tools/shared/Net";
2import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";2import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
3import { Config, Effect, FileSystem, LogLevel, Option, Path, Schema } from "effect";3import { Config, Effect, FileSystem, LogLevel, Option, Path, Schema } from "effect";
4import { Command, Flag, GlobalFlag } from "effect/unstable/cli";4import { Argument, Command, Flag, GlobalFlag } from "effect/unstable/cli";
55
6import {6import {
7 DEFAULT_PORT,7 DEFAULT_PORT,
...@@ -13,7 +13,7 @@ import {...@@ -13,7 +13,7 @@ import {
13 type ServerConfigShape,13 type ServerConfigShape,
14} from "./config";14} from "./config";
15import { readBootstrapEnvelope } from "./bootstrap";15import { readBootstrapEnvelope } from "./bootstrap";
16import { resolveBaseDir } from "./os-jank";16import { expandHomePath, resolveBaseDir } from "./os-jank";
17import { runServer } from "./server";17import { runServer } from "./server";
1818
19const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }));19const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }));
...@@ -140,6 +140,7 @@ interface CliServerFlags {...@@ -140,6 +140,7 @@ interface CliServerFlags {
140 readonly port: Option.Option<number>;140 readonly port: Option.Option<number>;
141 readonly host: Option.Option<string>;141 readonly host: Option.Option<string>;
142 readonly baseDir: Option.Option<string>;142 readonly baseDir: Option.Option<string>;
143 readonly cwd: Option.Option<string>;
143 readonly devUrl: Option.Option<URL>;144 readonly devUrl: Option.Option<URL>;
144 readonly noBrowser: Option.Option<boolean>;145 readonly noBrowser: Option.Option<boolean>;
145 readonly authToken: Option.Option<string>;146 readonly authToken: Option.Option<string>;
...@@ -225,6 +226,9 @@ export const resolveServerConfig = (...@@ -225,6 +226,9 @@ export const resolveServerConfig = (
225 ),226 ),
226 ),227 ),
227 );228 );
229 const rawCwd = Option.getOrElse(flags.cwd, () => process.cwd());
230 const cwd = path.resolve(yield* expandHomePath(rawCwd.trim()));
231 yield* fs.makeDirectory(cwd, { recursive: true });
228 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);232 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
229 yield* ensureServerDirectories(derivedPaths);233 yield* ensureServerDirectories(derivedPaths);
230 const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings(234 const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings(
...@@ -315,7 +319,7 @@ export const resolveServerConfig = (...@@ -315,7 +319,7 @@ export const resolveServerConfig = (
315 otlpServiceName: env.otlpServiceName,319 otlpServiceName: env.otlpServiceName,
316 mode,320 mode,
317 port,321 port,
318 cwd: process.cwd(),322 cwd,
319 baseDir,323 baseDir,
320 ...derivedPaths,324 ...derivedPaths,
321 serverTracePath,325 serverTracePath,
...@@ -336,6 +340,12 @@ const commandFlags = {...@@ -336,6 +340,12 @@ const commandFlags = {
336 port: portFlag,340 port: portFlag,
337 host: hostFlag,341 host: hostFlag,
338 baseDir: baseDirFlag,342 baseDir: baseDirFlag,
343 cwd: Argument.string("cwd").pipe(
344 Argument.withDescription(
345 "Working directory for provider sessions (defaults to the current directory).",
346 ),
347 Argument.optional,
348 ),
339 devUrl: devUrlFlag,349 devUrl: devUrlFlag,
340 noBrowser: noBrowserFlag,350 noBrowser: noBrowserFlag,
341 authToken: authTokenFlag,351 authToken: authTokenFlag,