Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ apps/*/dist
packages/*/dist
.env
.env.local
.t3-jira-config.json
build/
.logs/
release/
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"type": "module",
"scripts": {
"dev": "bun run src/bin.ts",
"build": "node scripts/cli.ts build",
"build": "node scripts/cli.mjs build",
"start": "node dist/bin.mjs",
"prepare": "effect-language-service patch",
"typecheck": "tsc --noEmit",
Expand Down
17 changes: 17 additions & 0 deletions apps/server/scripts/cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env node

import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const bunExecutable = process.env.npm_execpath ?? "bun";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium scripts/cli.mjs:6

When this script is invoked via npm run, process.env.npm_execpath contains npm's CLI script path (e.g., /usr/lib/node_modules/npm/bin/npm-cli.js), not a runtime executable. spawnSync then attempts to use that npm script to execute cli.ts, which fails because npm's CLI cannot directly run TypeScript files. The intent is to run cli.ts with bun, so the npm_execpath check is counterproductive when npm is the package manager. Consider removing the npm_execpath fallback or checking that the value is actually bun.

-const bunExecutable = process.env.npm_execpath ?? "bun";
+const bunExecutable = "bun";
Also found in 1 other location(s)

apps/server/scripts/cli.ts:147

The bunExecutable variable is assigned process.env.npm_execpath ?? process.execPath, but when running the build via npm (where npm_execpath points to npm's executable), the command npm tsdown will fail because npm doesn't accept tsdown as a direct subcommand. Similarly, the fallback process.execPath is the Node.js binary path, so node tsdown would also fail. This logic only works correctly when bun is the package manager.

🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/scripts/cli.mjs around line 6:

When this script is invoked via `npm run`, `process.env.npm_execpath` contains npm's CLI script path (e.g., `/usr/lib/node_modules/npm/bin/npm-cli.js`), not a runtime executable. `spawnSync` then attempts to use that npm script to execute `cli.ts`, which fails because npm's CLI cannot directly run TypeScript files. The intent is to run `cli.ts` with bun, so the `npm_execpath` check is counterproductive when npm is the package manager. Consider removing the `npm_execpath` fallback or checking that the value is actually bun.

Evidence trail:
apps/server/scripts/cli.mjs line 6 shows `const bunExecutable = process.env.npm_execpath ?? "bun";`. The variable name `bunExecutable` indicates intent to use bun. apps/server/package.json shows `"build": "node scripts/cli.mjs build"` which is invoked via `npm run build`. npm documentation confirms `npm_execpath` is set to the path of npm's executable (https://docs.npmjs.com/cli/v10/using-npm/scripts#environment). npm's CLI cannot execute TypeScript files directly - only a runtime like bun or ts-node can.

Also found in 1 other location(s):
- apps/server/scripts/cli.ts:147 -- The `bunExecutable` variable is assigned `process.env.npm_execpath ?? process.execPath`, but when running the build via npm (where `npm_execpath` points to npm's executable), the command `npm tsdown` will fail because npm doesn't accept `tsdown` as a direct subcommand. Similarly, the fallback `process.execPath` is the Node.js binary path, so `node tsdown` would also fail. This logic only works correctly when bun is the package manager.

const cliEntrypoint = fileURLToPath(new URL("./cli.ts", import.meta.url));

const result = spawnSync(bunExecutable, [cliEntrypoint, ...process.argv.slice(2)], {
stdio: "inherit",
});

if (result.error) {
throw result.error;
}

process.exit(result.status ?? 1);
5 changes: 3 additions & 2 deletions apps/server/scripts/cli.ts
Comment thread
brherron marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,17 @@ const buildCmd = Command.make(
const fs = yield* FileSystem.FileSystem;
const repoRoot = yield* RepoRoot;
const serverDir = path.join(repoRoot, "apps/server");
const bunExecutable = process.env.npm_execpath ?? process.execPath;

yield* Effect.log("[cli] Running tsdown...");
yield* runCommand(
ChildProcess.make({
ChildProcess.make(bunExecutable, ["tsdown"], {
cwd: serverDir,
stdout: config.verbose ? "inherit" : "ignore",
stderr: "inherit",
// Windows needs shell mode to resolve .cmd shims (e.g. bun.cmd).
shell: process.platform === "win32",
})`bun tsdown`,
}),
);

const webDist = path.join(repoRoot, "apps/web/dist");
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const deriveServerPaths = Effect.fn(function* (
anonymousIdPath: join(stateDir, "anonymous-id"),
environmentIdPath: join(stateDir, "environment-id"),
serverRuntimeStatePath: join(stateDir, "server-runtime.json"),
secretsDir: join(stateDir, "secrets"),
secretsDir: join(baseDir, "secrets"),
};
});

Expand Down
207 changes: 207 additions & 0 deletions apps/server/src/jira/Layers/JiraConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { readFile } from "node:fs/promises";
import path from "node:path";

import { Effect, Layer, Schema } from "effect";

import { JiraError, TrimmedNonEmptyString } from "@t3tools/contracts";
import { JiraConfig, type ResolvedJiraConfig } from "../Services/JiraConfig.ts";
import { runProcess } from "../../processRunner.ts";

const JiraAutomationConfigSchema = Schema.Struct({
enabled: Schema.Boolean,
transitionId: Schema.optional(TrimmedNonEmptyString),
});

const JiraFileConfigSchema = Schema.Struct({
baseUrl: TrimmedNonEmptyString,
email: TrimmedNonEmptyString,
token: TrimmedNonEmptyString,
automations: Schema.optional(
Schema.Struct({
on_branch_created: Schema.optional(JiraAutomationConfigSchema),
on_pr_opened: Schema.optional(JiraAutomationConfigSchema),
}),
),
});

function normalizeConfig(
input: typeof JiraFileConfigSchema.Type,
configPath: string,
): ResolvedJiraConfig {
let baseUrl: string;
try {
const url = new URL(input.baseUrl);
url.pathname = "";
url.search = "";
url.hash = "";
baseUrl = url.toString().replace(/\/+$/g, "");
} catch (cause) {
throw new JiraError({
kind: "config",
operation: "jira.config.parse",
message: "Invalid Jira baseUrl in .t3-jira-config.json.",
cause,
});
}

return {
configPath,
baseUrl,
email: input.email,
token: input.token,
automations: input.automations ?? {},
};
}

async function resolveConfigPath(cwd: string): Promise<string> {
const result = await runProcess("git", ["-C", cwd, "rev-parse", "--git-common-dir"], {
allowNonZeroExit: true,
});

if (result.code !== 0) {
throw new JiraError({
kind: "config",
operation: "jira.config.resolvePath",
message: "Jira config requires a git repository.",
});
}

const rawCommonDir = result.stdout.trim();
if (rawCommonDir.length === 0) {
throw new JiraError({
kind: "config",
operation: "jira.config.resolvePath",
message: "Could not resolve the shared git directory.",
});
}

const commonDir = path.resolve(cwd, rawCommonDir);
const repoRoot = path.basename(commonDir) === ".git" ? path.dirname(commonDir) : commonDir;
return path.join(repoRoot, ".t3-jira-config.json");
}

const parseJiraFileConfig = Schema.decodeUnknownSync(JiraFileConfigSchema);

async function loadResolvedConfig(cwd: string): Promise<ResolvedJiraConfig> {
const configPath = await resolveConfigPath(cwd);

let rawConfig: string;
try {
rawConfig = await readFile(configPath, "utf8");
} catch (cause) {
const code = (cause as NodeJS.ErrnoException | undefined)?.code;
if (code === "ENOENT") {
throw new JiraError({
kind: "config",
operation: "jira.config.read",
message: "Missing .t3-jira-config.json in the shared repository root.",
cause,
});
}

throw new JiraError({
kind: "config",
operation: "jira.config.read",
message: "Failed to read .t3-jira-config.json.",
cause,
});
}

let decoded: unknown;
try {
decoded = JSON.parse(rawConfig);
} catch (cause) {
throw new JiraError({
kind: "config",
operation: "jira.config.parse",
message: "Invalid Jira config JSON.",
cause,
});
}

try {
return normalizeConfig(parseJiraFileConfig(decoded), configPath);
} catch (cause) {
if (Schema.is(JiraError)(cause)) {
throw cause;
}

throw new JiraError({
kind: "config",
operation: "jira.config.parse",
message: "Invalid Jira config shape.",
cause,
});
}
}

export const makeJiraConfig = () =>
Layer.effect(
JiraConfig,
Effect.succeed({
getConfigStatus: (cwd: string) =>
Effect.tryPromise({
try: async () => {
let configPath: string;
try {
configPath = await resolveConfigPath(cwd);
} catch (cause) {
return {
status: "invalid" as const,
configPath: path.join(cwd, ".t3-jira-config.json"),
error:
cause instanceof Error ? cause.message : "Failed to resolve Jira config path.",
};
}

try {
await loadResolvedConfig(cwd);
return {
status: "ready" as const,
configPath,
};
} catch (cause) {
if (Schema.is(JiraError)(cause) && cause.kind === "config") {
return {
status: cause.message.includes("Missing .t3-jira-config.json")
? ("missing" as const)
: ("invalid" as const),
configPath,
...(cause.message.includes("Missing .t3-jira-config.json")
? {}
: { error: cause.message }),
};
}

return {
status: "invalid" as const,
configPath,
error: cause instanceof Error ? cause.message : "Failed to load Jira config.",
};
}
},
catch: (cause) =>
new JiraError({
kind: "config",
operation: "jira.config.status",
message: "Failed to inspect Jira config status.",
cause,
}),
}),
getResolvedConfig: (cwd: string) =>
Effect.tryPromise({
try: () => loadResolvedConfig(cwd),
catch: (cause) =>
Schema.is(JiraError)(cause)
? cause
: new JiraError({
kind: "config",
operation: "jira.config.load",
message: "Failed to load Jira config.",
cause,
}),
}),
}),
);

export const JiraConfigLive = makeJiraConfig();
Loading
Loading