Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
2c284d0f
Unverified
提交
2c284d0f
authored
7月 11, 2025
作者:
Will Chen
提交者:
GitHub
7月 11, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Allow configuring environmental variables in panel (#626)
- [ ] Add test cases
上级
4b84b12f
显示空白字符变更
内嵌
并排
正在显示
18 个修改的文件
包含
1216 行增加
和
7 行删除
+1216
-7
env_var.spec.ts
e2e-tests/env_var.spec.ts
+62
-0
test_helper.ts
e2e-tests/helpers/test_helper.ts
+1
-1
env_var.spec.ts_create-aKey
e2e-tests/snapshots/env_var.spec.ts_create-aKey
+2
-0
env_var.spec.ts_create-bKey
e2e-tests/snapshots/env_var.spec.ts_create-bKey
+3
-0
env_var.spec.ts_delete-aKey
e2e-tests/snapshots/env_var.spec.ts_delete-aKey
+2
-0
env_var.spec.ts_edit-bKey
e2e-tests/snapshots/env_var.spec.ts_edit-bKey
+3
-0
app_env_vars_utils.test.ts
src/__tests__/app_env_vars_utils.test.ts
+534
-0
TitleBar.tsx
src/app/TitleBar.tsx
+1
-1
appAtoms.ts
src/atoms/appAtoms.ts
+3
-1
ConfigurePanel.tsx
src/components/preview_panel/ConfigurePanel.tsx
+401
-0
PreviewHeader.tsx
src/components/preview_panel/PreviewHeader.tsx
+19
-4
PreviewPanel.tsx
src/components/preview_panel/PreviewPanel.tsx
+3
-0
app_env_vars_handlers.ts
src/ipc/handlers/app_env_vars_handlers.ts
+81
-0
ipc_client.ts
src/ipc/ipc_client.ts
+12
-0
ipc_host.ts
src/ipc/ipc_host.ts
+2
-0
ipc_types.ts
src/ipc/ipc_types.ts
+14
-0
app_env_var_utils.ts
src/ipc/utils/app_env_var_utils.ts
+71
-0
preload.ts
src/preload.ts
+2
-0
没有找到文件。
e2e-tests/env_var.spec.ts
0 → 100644
浏览文件 @
2c284d0f
import
{
expect
}
from
"@playwright/test"
;
import
{
test
}
from
"./helpers/test_helper"
;
import
path
from
"path"
;
import
fs
from
"fs"
;
test
(
"env var"
,
async
({
po
})
=>
{
await
po
.
sendPrompt
(
"tc=1"
);
const
appPath
=
await
po
.
getCurrentAppPath
();
await
po
.
selectPreviewMode
(
"configure"
);
// Create a new env var
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Add Environment Variable"
})
.
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Key"
}).
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Key"
}).
fill
(
"aKey"
);
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Value"
}).
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Value"
}).
fill
(
"aValue"
);
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Save"
}).
click
();
await
snapshotEnvVar
({
appPath
,
name
:
"create-aKey"
});
// Create second env var
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Add Environment Variable"
})
.
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Key"
}).
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Key"
}).
fill
(
"bKey"
);
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Value"
}).
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Value"
}).
fill
(
"bValue"
);
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Save"
}).
click
();
await
snapshotEnvVar
({
appPath
,
name
:
"create-bKey"
});
// Edit second env var
await
po
.
page
.
getByTestId
(
"edit-env-var-bKey"
).
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Value"
}).
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Value"
}).
fill
(
"bValue2"
);
await
po
.
page
.
getByTestId
(
"save-edit-env-var"
).
click
();
await
snapshotEnvVar
({
appPath
,
name
:
"edit-bKey"
});
// Delete first env var
await
po
.
page
.
getByTestId
(
"delete-env-var-aKey"
).
click
();
await
snapshotEnvVar
({
appPath
,
name
:
"delete-aKey"
});
});
async
function
snapshotEnvVar
({
appPath
,
name
,
}:
{
appPath
:
string
;
name
:
string
;
})
{
expect
(()
=>
{
const
envFile
=
path
.
join
(
appPath
,
".env.local"
);
const
envFileContent
=
fs
.
readFileSync
(
envFile
,
"utf8"
);
expect
(
envFileContent
).
toMatchSnapshot
({
name
});
}).
toPass
();
}
e2e-tests/helpers/test_helper.ts
浏览文件 @
2c284d0f
...
...
@@ -422,7 +422,7 @@ export class PageObject {
// Preview panel
////////////////////////////////
async
selectPreviewMode
(
mode
:
"code"
|
"problems"
|
"preview"
)
{
async
selectPreviewMode
(
mode
:
"code"
|
"problems"
|
"preview"
|
"configure"
)
{
await
this
.
page
.
getByTestId
(
`
${
mode
}
-mode-button`
).
click
();
}
...
...
e2e-tests/snapshots/env_var.spec.ts_create-aKey
0 → 100644
浏览文件 @
2c284d0f
aKey=aValue
\ No newline at end of file
e2e-tests/snapshots/env_var.spec.ts_create-bKey
0 → 100644
浏览文件 @
2c284d0f
aKey=aValue
bKey=bValue
\ No newline at end of file
e2e-tests/snapshots/env_var.spec.ts_delete-aKey
0 → 100644
浏览文件 @
2c284d0f
bKey=bValue2
\ No newline at end of file
e2e-tests/snapshots/env_var.spec.ts_edit-bKey
0 → 100644
浏览文件 @
2c284d0f
aKey=aValue
bKey=bValue2
\ No newline at end of file
src/__tests__/app_env_vars_utils.test.ts
0 → 100644
浏览文件 @
2c284d0f
import
{
parseEnvFile
,
serializeEnvFile
}
from
"@/ipc/utils/app_env_var_utils"
;
import
{
describe
,
it
,
expect
}
from
"vitest"
;
describe
(
"parseEnvFile"
,
()
=>
{
it
(
"should parse basic key=value pairs"
,
()
=>
{
const
content
=
`API_KEY=abc123
DATABASE_URL=postgres://localhost:5432/mydb
PORT=3000`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
{
key
:
"PORT"
,
value
:
"3000"
},
]);
});
it
(
"should handle quoted values and remove quotes"
,
()
=>
{
const
content
=
`API_KEY="abc123"
DATABASE_URL='postgres://localhost:5432/mydb'
MESSAGE="Hello World"`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
{
key
:
"MESSAGE"
,
value
:
"Hello World"
},
]);
});
it
(
"should skip empty lines"
,
()
=>
{
const
content
=
`API_KEY=abc123
DATABASE_URL=postgres://localhost:5432/mydb
PORT=3000`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
{
key
:
"PORT"
,
value
:
"3000"
},
]);
});
it
(
"should skip comment lines"
,
()
=>
{
const
content
=
`# This is a comment
API_KEY=abc123
# Another comment
DATABASE_URL=postgres://localhost:5432/mydb
# PORT=3000 (commented out)
DEBUG=true`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
{
key
:
"DEBUG"
,
value
:
"true"
},
]);
});
it
(
"should handle values with spaces"
,
()
=>
{
const
content
=
`MESSAGE="Hello World"
DESCRIPTION='This is a long description'
TITLE=My App Title`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"MESSAGE"
,
value
:
"Hello World"
},
{
key
:
"DESCRIPTION"
,
value
:
"This is a long description"
},
{
key
:
"TITLE"
,
value
:
"My App Title"
},
]);
});
it
(
"should handle values with special characters"
,
()
=>
{
const
content
=
`PASSWORD="p@ssw0rd!#$%"
URL="https://example.com/api?key=123&secret=456"
REGEX="^[a-zA-Z0-9]+$"`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"PASSWORD"
,
value
:
"p@ssw0rd!#$%"
},
{
key
:
"URL"
,
value
:
"https://example.com/api?key=123&secret=456"
},
{
key
:
"REGEX"
,
value
:
"^[a-zA-Z0-9]+$"
},
]);
});
it
(
"should handle empty values"
,
()
=>
{
const
content
=
`EMPTY_VAR=
QUOTED_EMPTY=""
ANOTHER_VAR=value`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"EMPTY_VAR"
,
value
:
""
},
{
key
:
"QUOTED_EMPTY"
,
value
:
""
},
{
key
:
"ANOTHER_VAR"
,
value
:
"value"
},
]);
});
it
(
"should handle values with equals signs"
,
()
=>
{
const
content
=
`EQUATION="2+2=4"
CONNECTION_STRING="server=localhost;user=admin;password=secret"`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"EQUATION"
,
value
:
"2+2=4"
},
{
key
:
"CONNECTION_STRING"
,
value
:
"server=localhost;user=admin;password=secret"
,
},
]);
});
it
(
"should trim whitespace around keys and values"
,
()
=>
{
const
content
=
` API_KEY = abc123
DATABASE_URL = "postgres://localhost:5432/mydb"
PORT = 3000 `
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
{
key
:
"PORT"
,
value
:
"3000"
},
]);
});
it
(
"should skip malformed lines without equals sign"
,
()
=>
{
const
content
=
`API_KEY=abc123
MALFORMED_LINE
DATABASE_URL=postgres://localhost:5432/mydb
ANOTHER_MALFORMED
PORT=3000`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
{
key
:
"PORT"
,
value
:
"3000"
},
]);
});
it
(
"should skip lines with equals sign at the beginning"
,
()
=>
{
const
content
=
`API_KEY=abc123
=invalid_line
DATABASE_URL=postgres://localhost:5432/mydb`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
]);
});
it
(
"should handle mixed quote types in values"
,
()
=>
{
const
content
=
`MESSAGE="He said 'Hello World'"
COMMAND='echo "Hello World"'`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"MESSAGE"
,
value
:
"He said 'Hello World'"
},
{
key
:
"COMMAND"
,
value
:
'echo "Hello World"'
},
]);
});
it
(
"should handle empty content"
,
()
=>
{
const
result
=
parseEnvFile
(
""
);
expect
(
result
).
toEqual
([]);
});
it
(
"should handle content with only comments and empty lines"
,
()
=>
{
const
content
=
`# Comment 1
# Comment 2
# Comment 3`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([]);
});
it
(
"should handle values that start with hash symbol when quoted"
,
()
=>
{
const
content
=
`HASH_VALUE="#hashtag"
COMMENT_LIKE="# This looks like a comment but it's a value"
ACTUAL_COMMENT=value
# This is an actual comment`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"HASH_VALUE"
,
value
:
"#hashtag"
},
{
key
:
"COMMENT_LIKE"
,
value
:
"# This looks like a comment but it's a value"
,
},
{
key
:
"ACTUAL_COMMENT"
,
value
:
"value"
},
]);
});
it
(
"should skip comments that look like key=value pairs"
,
()
=>
{
const
content
=
`API_KEY=abc123
# SECRET_KEY=should_be_ignored
DATABASE_URL=postgres://localhost:5432/mydb
# PORT=3000
DEBUG=true`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
{
key
:
"DEBUG"
,
value
:
"true"
},
]);
});
it
(
"should handle values containing comment symbols"
,
()
=>
{
const
content
=
`GIT_COMMIT_MSG="feat: add new feature # closes #123"
SQL_QUERY="SELECT * FROM users WHERE id = 1 # Get user by ID"
MARKDOWN_HEADING="# Main Title"
SHELL_COMMENT="echo 'hello' # prints hello"`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"GIT_COMMIT_MSG"
,
value
:
"feat: add new feature # closes #123"
},
{
key
:
"SQL_QUERY"
,
value
:
"SELECT * FROM users WHERE id = 1 # Get user by ID"
,
},
{
key
:
"MARKDOWN_HEADING"
,
value
:
"# Main Title"
},
{
key
:
"SHELL_COMMENT"
,
value
:
"echo 'hello' # prints hello"
},
]);
});
it
(
"should handle inline comments after key=value pairs"
,
()
=>
{
const
content
=
`API_KEY=abc123 # This is the API key
DATABASE_URL=postgres://localhost:5432/mydb # Database connection
PORT=3000 # Server port
DEBUG=true # Enable debug mode`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123 # This is the API key"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb # Database connection"
,
},
{
key
:
"PORT"
,
value
:
"3000 # Server port"
},
{
key
:
"DEBUG"
,
value
:
"true # Enable debug mode"
},
]);
});
it
(
"should handle quoted values with inline comments"
,
()
=>
{
const
content
=
`MESSAGE="Hello World" # Greeting message
PASSWORD="secret#123" # Password with hash
URL="https://example.com#section" # URL with fragment`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"MESSAGE"
,
value
:
"Hello World"
},
{
key
:
"PASSWORD"
,
value
:
"secret#123"
},
{
key
:
"URL"
,
value
:
"https://example.com#section"
},
]);
});
it
(
"should handle complex mixed comment scenarios"
,
()
=>
{
const
content
=
`# Configuration file
API_KEY=abc123
# Database settings
DATABASE_URL="postgres://localhost:5432/mydb"
# PORT=5432 (commented out)
DATABASE_NAME=myapp
# Feature flags
FEATURE_A=true # Enable feature A
FEATURE_B="false" # Disable feature B
# FEATURE_C=true (disabled)
# URLs with fragments
HOMEPAGE="https://example.com#home"
DOCS_URL=https://docs.example.com#getting-started # Documentation link`
;
const
result
=
parseEnvFile
(
content
);
expect
(
result
).
toEqual
([
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
{
key
:
"DATABASE_NAME"
,
value
:
"myapp"
},
{
key
:
"FEATURE_A"
,
value
:
"true # Enable feature A"
},
{
key
:
"FEATURE_B"
,
value
:
"false"
},
{
key
:
"HOMEPAGE"
,
value
:
"https://example.com#home"
},
{
key
:
"DOCS_URL"
,
value
:
"https://docs.example.com#getting-started # Documentation link"
,
},
]);
});
});
describe
(
"serializeEnvFile"
,
()
=>
{
it
(
"should serialize basic key=value pairs"
,
()
=>
{
const
envVars
=
[
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"DATABASE_URL"
,
value
:
"postgres://localhost:5432/mydb"
},
{
key
:
"PORT"
,
value
:
"3000"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`API_KEY=abc123
DATABASE_URL=postgres://localhost:5432/mydb
PORT=3000`
);
});
it
(
"should quote values with spaces"
,
()
=>
{
const
envVars
=
[
{
key
:
"MESSAGE"
,
value
:
"Hello World"
},
{
key
:
"DESCRIPTION"
,
value
:
"This is a long description"
},
{
key
:
"SIMPLE"
,
value
:
"no_spaces"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`MESSAGE="Hello World"
DESCRIPTION="This is a long description"
SIMPLE=no_spaces`
);
});
it
(
"should quote values with special characters"
,
()
=>
{
const
envVars
=
[
{
key
:
"PASSWORD"
,
value
:
"p@ssw0rd!#$%"
},
{
key
:
"URL"
,
value
:
"https://example.com/api?key=123&secret=456"
},
{
key
:
"SIMPLE"
,
value
:
"simple123"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`PASSWORD="p@ssw0rd!#$%"
URL="https://example.com/api?key=123&secret=456"
SIMPLE=simple123`
);
});
it
(
"should escape quotes in values"
,
()
=>
{
const
envVars
=
[
{
key
:
"MESSAGE"
,
value
:
'He said "Hello World"'
},
{
key
:
"COMMAND"
,
value
:
'echo "test"'
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`MESSAGE="He said \\"Hello World\\""
COMMAND="echo \\"test\\""`
);
});
it
(
"should handle empty values"
,
()
=>
{
const
envVars
=
[
{
key
:
"EMPTY_VAR"
,
value
:
""
},
{
key
:
"ANOTHER_VAR"
,
value
:
"value"
},
{
key
:
"ALSO_EMPTY"
,
value
:
""
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`EMPTY_VAR=
ANOTHER_VAR=value
ALSO_EMPTY=`
);
});
it
(
"should quote values with hash symbols"
,
()
=>
{
const
envVars
=
[
{
key
:
"PASSWORD"
,
value
:
"secret#123"
},
{
key
:
"COMMENT"
,
value
:
"This has # in it"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`PASSWORD="secret#123"
COMMENT="This has # in it"`
);
});
it
(
"should quote values with single quotes"
,
()
=>
{
const
envVars
=
[
{
key
:
"MESSAGE"
,
value
:
"Don't worry"
},
{
key
:
"SQL"
,
value
:
"SELECT * FROM 'users'"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`MESSAGE="Don't worry"
SQL="SELECT * FROM 'users'"`
);
});
it
(
"should handle values with equals signs"
,
()
=>
{
const
envVars
=
[
{
key
:
"EQUATION"
,
value
:
"2+2=4"
},
{
key
:
"CONNECTION_STRING"
,
value
:
"server=localhost;user=admin;password=secret"
,
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`EQUATION="2+2=4"
CONNECTION_STRING="server=localhost;user=admin;password=secret"`
);
});
it
(
"should handle mixed scenarios"
,
()
=>
{
const
envVars
=
[
{
key
:
"SIMPLE"
,
value
:
"value"
},
{
key
:
"WITH_SPACES"
,
value
:
"hello world"
},
{
key
:
"WITH_QUOTES"
,
value
:
'say "hello"'
},
{
key
:
"EMPTY"
,
value
:
""
},
{
key
:
"SPECIAL_CHARS"
,
value
:
"p@ssw0rd!#$%"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`SIMPLE=value
WITH_SPACES="hello world"
WITH_QUOTES="say \\"hello\\""
EMPTY=
SPECIAL_CHARS="p@ssw0rd!#$%"`
);
});
it
(
"should handle empty array"
,
()
=>
{
const
result
=
serializeEnvFile
([]);
expect
(
result
).
toBe
(
""
);
});
it
(
"should handle complex escaped quotes"
,
()
=>
{
const
envVars
=
[
{
key
:
"COMPLEX"
,
value
:
"This is
\"
complex
\"
with 'mixed' quotes"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`COMPLEX="This is \\"complex\\" with 'mixed' quotes"`
);
});
it
(
"should handle values that start with hash symbol"
,
()
=>
{
const
envVars
=
[
{
key
:
"HASHTAG"
,
value
:
"#trending"
},
{
key
:
"COMMENT_LIKE"
,
value
:
"# This looks like a comment"
},
{
key
:
"MARKDOWN_HEADING"
,
value
:
"# Main Title"
},
{
key
:
"NORMAL_VALUE"
,
value
:
"no_hash_here"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`HASHTAG="#trending"
COMMENT_LIKE="# This looks like a comment"
MARKDOWN_HEADING="# Main Title"
NORMAL_VALUE=no_hash_here`
);
});
it
(
"should handle values containing comment symbols"
,
()
=>
{
const
envVars
=
[
{
key
:
"GIT_COMMIT"
,
value
:
"feat: add feature # closes #123"
},
{
key
:
"SQL_QUERY"
,
value
:
"SELECT * FROM users # Get all users"
},
{
key
:
"SHELL_CMD"
,
value
:
"echo 'hello' # prints hello"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`GIT_COMMIT="feat: add feature # closes #123"
SQL_QUERY="SELECT * FROM users # Get all users"
SHELL_CMD="echo 'hello' # prints hello"`
);
});
it
(
"should handle URLs with fragments that contain hash symbols"
,
()
=>
{
const
envVars
=
[
{
key
:
"HOMEPAGE"
,
value
:
"https://example.com#home"
},
{
key
:
"DOCS_URL"
,
value
:
"https://docs.example.com#getting-started"
},
{
key
:
"API_ENDPOINT"
,
value
:
"https://api.example.com/v1#section"
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`HOMEPAGE="https://example.com#home"
DOCS_URL="https://docs.example.com#getting-started"
API_ENDPOINT="https://api.example.com/v1#section"`
);
});
it
(
"should handle values with hash symbols and other special characters"
,
()
=>
{
const
envVars
=
[
{
key
:
"COMPLEX_PASSWORD"
,
value
:
"p@ssw0rd#123!&"
},
{
key
:
"REGEX_PATTERN"
,
value
:
"^[a-zA-Z0-9#]+$"
},
{
key
:
"MARKDOWN_CONTENT"
,
value
:
"# Title
\
n
\
nSome content with = and & symbols"
,
},
];
const
result
=
serializeEnvFile
(
envVars
);
expect
(
result
).
toBe
(
`COMPLEX_PASSWORD="p@ssw0rd#123!&"
REGEX_PATTERN="^[a-zA-Z0-9#]+$"
MARKDOWN_CONTENT="# Title\n\nSome content with = and & symbols"`
);
});
});
describe
(
"parseEnvFile and serializeEnvFile integration"
,
()
=>
{
it
(
"should be able to parse what it serializes"
,
()
=>
{
const
originalEnvVars
=
[
{
key
:
"API_KEY"
,
value
:
"abc123"
},
{
key
:
"MESSAGE"
,
value
:
"Hello World"
},
{
key
:
"PASSWORD"
,
value
:
'secret"123'
},
{
key
:
"EMPTY"
,
value
:
""
},
{
key
:
"SPECIAL"
,
value
:
"p@ssw0rd!#$%"
},
];
const
serialized
=
serializeEnvFile
(
originalEnvVars
);
const
parsed
=
parseEnvFile
(
serialized
);
expect
(
parsed
).
toEqual
(
originalEnvVars
);
});
it
(
"should handle round-trip with complex values"
,
()
=>
{
const
originalEnvVars
=
[
{
key
:
"URL"
,
value
:
"https://example.com/api?key=123&secret=456"
},
{
key
:
"REGEX"
,
value
:
"^[a-zA-Z0-9]+$"
},
{
key
:
"COMMAND"
,
value
:
'echo "Hello World"'
},
{
key
:
"EQUATION"
,
value
:
"2+2=4"
},
];
const
serialized
=
serializeEnvFile
(
originalEnvVars
);
const
parsed
=
parseEnvFile
(
serialized
);
expect
(
parsed
).
toEqual
(
originalEnvVars
);
});
it
(
"should handle round-trip with comment-like values"
,
()
=>
{
const
originalEnvVars
=
[
{
key
:
"HASHTAG"
,
value
:
"#trending"
},
{
key
:
"COMMENT_LIKE"
,
value
:
"# This looks like a comment but it's a value"
,
},
{
key
:
"GIT_COMMIT"
,
value
:
"feat: add feature # closes #123"
},
{
key
:
"URL_WITH_FRAGMENT"
,
value
:
"https://example.com#section"
},
{
key
:
"MARKDOWN_HEADING"
,
value
:
"# Main Title"
},
{
key
:
"COMPLEX_VALUE"
,
value
:
"password#123=secret&token=abc"
},
];
const
serialized
=
serializeEnvFile
(
originalEnvVars
);
const
parsed
=
parseEnvFile
(
serialized
);
expect
(
parsed
).
toEqual
(
originalEnvVars
);
});
});
src/app/TitleBar.tsx
浏览文件 @
2c284d0f
...
...
@@ -228,7 +228,7 @@ export function AICreditStatus({ userBudget }: { userBudget: UserBudgetInfo }) {
return
(
<
Tooltip
>
<
TooltipTrigger
>
<
div
className=
"text-xs mt-0.5"
>
{
remaining
}
credits
left
</
div
>
<
div
className=
"text-xs mt-0.5"
>
{
remaining
}
credits
</
div
>
</
TooltipTrigger
>
<
TooltipContent
>
<
div
>
...
...
src/atoms/appAtoms.ts
浏览文件 @
2c284d0f
...
...
@@ -7,7 +7,9 @@ export const selectedAppIdAtom = atom<number | null>(null);
export
const
appsListAtom
=
atom
<
App
[]
>
([]);
export
const
appBasePathAtom
=
atom
<
string
>
(
""
);
export
const
versionsListAtom
=
atom
<
Version
[]
>
([]);
export
const
previewModeAtom
=
atom
<
"preview"
|
"code"
|
"problems"
>
(
"preview"
);
export
const
previewModeAtom
=
atom
<
"preview"
|
"code"
|
"problems"
|
"configure"
>
(
"preview"
);
export
const
selectedVersionIdAtom
=
atom
<
string
|
null
>
(
null
);
export
const
appOutputAtom
=
atom
<
AppOutput
[]
>
([]);
export
const
appUrlAtom
=
atom
<
...
...
src/components/preview_panel/ConfigurePanel.tsx
0 → 100644
浏览文件 @
2c284d0f
import
{
useState
,
useCallback
}
from
"react"
;
import
{
useQuery
,
useMutation
,
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
useAtomValue
}
from
"jotai"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
Card
,
CardContent
,
CardHeader
,
CardTitle
}
from
"@/components/ui/card"
;
import
{
Tooltip
,
TooltipTrigger
,
TooltipContent
,
}
from
"@/components/ui/tooltip"
;
import
{
Trash2
,
Edit2
,
Plus
,
Save
,
X
,
HelpCircle
,
ArrowRight
,
}
from
"lucide-react"
;
import
{
showError
,
showSuccess
}
from
"@/lib/toast"
;
import
{
selectedAppIdAtom
}
from
"@/atoms/appAtoms"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
useNavigate
}
from
"@tanstack/react-router"
;
const
EnvironmentVariablesTitle
=
()
=>
(
<
div
className=
"flex items-center gap-2"
>
<
span
className=
"text-lg font-semibold"
>
Environment Variables
</
span
>
<
span
className=
"text-sm text-muted-foreground font-normal"
>
Local
</
span
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
HelpCircle
size=
{
16
}
className=
"text-muted-foreground cursor-help"
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
To modify environment variables for Supabase or production,
<
br
/>
access your hosting provider's console and update them there.
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
div
>
);
export
const
ConfigurePanel
=
()
=>
{
const
selectedAppId
=
useAtomValue
(
selectedAppIdAtom
);
const
queryClient
=
useQueryClient
();
const
[
editingKey
,
setEditingKey
]
=
useState
<
string
|
null
>
(
null
);
const
[
editingKeyValue
,
setEditingKeyValue
]
=
useState
(
""
);
const
[
editingValue
,
setEditingValue
]
=
useState
(
""
);
const
[
newKey
,
setNewKey
]
=
useState
(
""
);
const
[
newValue
,
setNewValue
]
=
useState
(
""
);
const
[
isAddingNew
,
setIsAddingNew
]
=
useState
(
false
);
const
navigate
=
useNavigate
();
// Query to get environment variables
const
{
data
:
envVars
=
[],
isLoading
,
error
,
}
=
useQuery
({
queryKey
:
[
"app-env-vars"
,
selectedAppId
],
queryFn
:
async
()
=>
{
if
(
!
selectedAppId
)
return
[];
const
ipcClient
=
IpcClient
.
getInstance
();
return
await
ipcClient
.
getAppEnvVars
({
appId
:
selectedAppId
});
},
enabled
:
!!
selectedAppId
,
});
// Mutation to save environment variables
const
saveEnvVarsMutation
=
useMutation
({
mutationFn
:
async
(
newEnvVars
:
{
key
:
string
;
value
:
string
}[])
=>
{
if
(
!
selectedAppId
)
throw
new
Error
(
"No app selected"
);
const
ipcClient
=
IpcClient
.
getInstance
();
return
await
ipcClient
.
setAppEnvVars
({
appId
:
selectedAppId
,
envVars
:
newEnvVars
,
});
},
onSuccess
:
()
=>
{
queryClient
.
invalidateQueries
({
queryKey
:
[
"app-env-vars"
,
selectedAppId
],
});
showSuccess
(
"Environment variables saved"
);
},
onError
:
(
error
)
=>
{
showError
(
`Failed to save environment variables:
${
error
}
`
);
},
});
const
handleAdd
=
useCallback
(()
=>
{
if
(
!
newKey
.
trim
()
||
!
newValue
.
trim
())
{
showError
(
"Both key and value are required"
);
return
;
}
// Check for duplicate keys
if
(
envVars
.
some
((
envVar
)
=>
envVar
.
key
===
newKey
.
trim
()))
{
showError
(
"Environment variable with this key already exists"
);
return
;
}
const
newEnvVars
=
[
...
envVars
,
{
key
:
newKey
.
trim
(),
value
:
newValue
.
trim
()
},
];
saveEnvVarsMutation
.
mutate
(
newEnvVars
);
setNewKey
(
""
);
setNewValue
(
""
);
setIsAddingNew
(
false
);
},
[
newKey
,
newValue
,
envVars
,
saveEnvVarsMutation
]);
const
handleEdit
=
useCallback
((
envVar
:
{
key
:
string
;
value
:
string
})
=>
{
setEditingKey
(
envVar
.
key
);
setEditingKeyValue
(
envVar
.
key
);
setEditingValue
(
envVar
.
value
);
},
[]);
const
handleSaveEdit
=
useCallback
(()
=>
{
if
(
!
editingKeyValue
.
trim
()
||
!
editingValue
.
trim
())
{
showError
(
"Both key and value are required"
);
return
;
}
// Check for duplicate keys (excluding the current one being edited)
if
(
envVars
.
some
(
(
envVar
)
=>
envVar
.
key
===
editingKeyValue
.
trim
()
&&
envVar
.
key
!==
editingKey
,
)
)
{
showError
(
"Environment variable with this key already exists"
);
return
;
}
const
newEnvVars
=
envVars
.
map
((
envVar
)
=>
envVar
.
key
===
editingKey
?
{
key
:
editingKeyValue
.
trim
(),
value
:
editingValue
.
trim
()
}
:
envVar
,
);
saveEnvVarsMutation
.
mutate
(
newEnvVars
);
setEditingKey
(
null
);
setEditingKeyValue
(
""
);
setEditingValue
(
""
);
},
[
editingKey
,
editingKeyValue
,
editingValue
,
envVars
,
saveEnvVarsMutation
]);
const
handleCancelEdit
=
useCallback
(()
=>
{
setEditingKey
(
null
);
setEditingKeyValue
(
""
);
setEditingValue
(
""
);
},
[]);
const
handleDelete
=
useCallback
(
(
key
:
string
)
=>
{
const
newEnvVars
=
envVars
.
filter
((
envVar
)
=>
envVar
.
key
!==
key
);
saveEnvVarsMutation
.
mutate
(
newEnvVars
);
},
[
envVars
,
saveEnvVarsMutation
],
);
const
handleCancelAdd
=
useCallback
(()
=>
{
setIsAddingNew
(
false
);
setNewKey
(
""
);
setNewValue
(
""
);
},
[]);
// Show loading state
if
(
isLoading
)
{
return
(
<
div
className=
"p-4 space-y-4"
>
<
Card
>
<
CardHeader
>
<
CardTitle
>
<
EnvironmentVariablesTitle
/>
</
CardTitle
>
</
CardHeader
>
<
CardContent
>
<
div
className=
"text-center py-8"
>
<
div
className=
"text-sm text-muted-foreground"
>
Loading environment variables...
</
div
>
</
div
>
</
CardContent
>
</
Card
>
</
div
>
);
}
// Show error state
if
(
error
)
{
return
(
<
div
className=
"p-4 space-y-4"
>
<
Card
>
<
CardHeader
>
<
CardTitle
>
<
EnvironmentVariablesTitle
/>
</
CardTitle
>
</
CardHeader
>
<
CardContent
>
<
div
className=
"text-center py-8"
>
<
div
className=
"text-sm text-red-500"
>
Error loading environment variables:
{
error
.
message
}
</
div
>
</
div
>
</
CardContent
>
</
Card
>
</
div
>
);
}
// Show no app selected state
if
(
!
selectedAppId
)
{
return
(
<
div
className=
"p-4 space-y-4"
>
<
Card
>
<
CardHeader
>
<
CardTitle
>
<
EnvironmentVariablesTitle
/>
</
CardTitle
>
</
CardHeader
>
<
CardContent
>
<
div
className=
"text-center py-8"
>
<
div
className=
"text-sm text-muted-foreground"
>
Select an app to manage environment variables
</
div
>
</
div
>
</
CardContent
>
</
Card
>
</
div
>
);
}
return
(
<
div
className=
"p-4 space-y-4"
>
<
Card
>
<
CardHeader
>
<
CardTitle
>
<
EnvironmentVariablesTitle
/>
</
CardTitle
>
</
CardHeader
>
<
CardContent
className=
"space-y-4"
>
{
/* Add new environment variable form */
}
{
isAddingNew
?
(
<
div
className=
"space-y-3 p-3 border rounded-md bg-muted/50"
>
<
div
className=
"space-y-2"
>
<
Label
htmlFor=
"new-key"
>
Key
</
Label
>
<
Input
id=
"new-key"
placeholder=
"e.g., API_URL"
value=
{
newKey
}
onChange=
{
(
e
)
=>
setNewKey
(
e
.
target
.
value
)
}
autoFocus
/>
</
div
>
<
div
className=
"space-y-2"
>
<
Label
htmlFor=
"new-value"
>
Value
</
Label
>
<
Input
id=
"new-value"
placeholder=
"e.g., https://api.example.com"
value=
{
newValue
}
onChange=
{
(
e
)
=>
setNewValue
(
e
.
target
.
value
)
}
/>
</
div
>
<
div
className=
"flex gap-2"
>
<
Button
onClick=
{
handleAdd
}
size=
"sm"
disabled=
{
saveEnvVarsMutation
.
isPending
}
>
<
Save
size=
{
14
}
/>
{
saveEnvVarsMutation
.
isPending
?
"Saving..."
:
"Save"
}
</
Button
>
<
Button
onClick=
{
handleCancelAdd
}
variant=
"outline"
size=
"sm"
>
<
X
size=
{
14
}
/>
Cancel
</
Button
>
</
div
>
</
div
>
)
:
(
<
Button
onClick=
{
()
=>
setIsAddingNew
(
true
)
}
variant=
"outline"
className=
"w-full"
>
<
Plus
size=
{
14
}
/>
Add Environment Variable
</
Button
>
)
}
{
/* List of existing environment variables */
}
<
div
className=
"space-y-2"
>
{
envVars
.
length
===
0
?
(
<
p
className=
"text-sm text-muted-foreground text-center py-8"
>
No environment variables configured
</
p
>
)
:
(
envVars
.
map
((
envVar
)
=>
(
<
div
key=
{
envVar
.
key
}
className=
"flex items-center space-x-2 p-2 border rounded-md"
>
{
editingKey
===
envVar
.
key
?
(
<>
<
div
className=
"flex-1 space-y-2"
>
<
Input
value=
{
editingKeyValue
}
onChange=
{
(
e
)
=>
setEditingKeyValue
(
e
.
target
.
value
)
}
placeholder=
"Key"
className=
"h-8"
/>
<
Input
value=
{
editingValue
}
onChange=
{
(
e
)
=>
setEditingValue
(
e
.
target
.
value
)
}
placeholder=
"Value"
className=
"h-8"
/>
</
div
>
<
div
className=
"flex gap-1"
>
<
Button
data
-
testid=
{
`save-edit-env-var`
}
onClick=
{
handleSaveEdit
}
size=
"sm"
variant=
"outline"
disabled=
{
saveEnvVarsMutation
.
isPending
}
>
<
Save
size=
{
14
}
/>
</
Button
>
<
Button
data
-
testid=
{
`cancel-edit-env-var`
}
onClick=
{
handleCancelEdit
}
size=
"sm"
variant=
"outline"
>
<
X
size=
{
14
}
/>
</
Button
>
</
div
>
</>
)
:
(
<>
<
div
className=
"flex-1 min-w-0"
>
<
div
className=
"font-medium text-sm truncate"
>
{
envVar
.
key
}
</
div
>
<
div
className=
"text-xs text-muted-foreground truncate"
>
{
envVar
.
value
}
</
div
>
</
div
>
<
div
className=
"flex gap-1"
>
<
Button
data
-
testid=
{
`edit-env-var-${envVar.key}`
}
onClick=
{
()
=>
handleEdit
(
envVar
)
}
size=
"sm"
variant=
"ghost"
className=
"h-8 w-8 p-0"
>
<
Edit2
size=
{
14
}
/>
</
Button
>
<
Button
data
-
testid=
{
`delete-env-var-${envVar.key}`
}
onClick=
{
()
=>
handleDelete
(
envVar
.
key
)
}
size=
"sm"
variant=
"ghost"
className=
"h-8 w-8 p-0 text-destructive hover:text-destructive"
disabled=
{
saveEnvVarsMutation
.
isPending
}
>
<
Trash2
size=
{
14
}
/>
</
Button
>
</
div
>
</>
)
}
</
div
>
))
)
}
</
div
>
{
/* More app configurations button */
}
<
div
className=
"pt-4 border-t"
>
<
Button
variant=
"outline"
className=
"w-full text-sm justify-between"
onClick=
{
()
=>
{
if
(
selectedAppId
)
{
navigate
({
to
:
"/app-details"
,
search
:
{
appId
:
selectedAppId
},
});
}
}
}
>
<
span
>
More app settings
</
span
>
<
ArrowRight
size=
{
16
}
/>
</
Button
>
</
div
>
</
CardContent
>
</
Card
>
</
div
>
);
};
src/components/preview_panel/PreviewHeader.tsx
浏览文件 @
2c284d0f
...
...
@@ -9,6 +9,7 @@ import {
Cog
,
Trash2
,
AlertTriangle
,
Wrench
,
}
from
"lucide-react"
;
import
{
motion
}
from
"framer-motion"
;
import
{
useEffect
,
useRef
,
useState
,
useCallback
}
from
"react"
;
...
...
@@ -25,7 +26,7 @@ import { useMutation } from "@tanstack/react-query";
import
{
useCheckProblems
}
from
"@/hooks/useCheckProblems"
;
import
{
isPreviewOpenAtom
}
from
"@/atoms/viewAtoms"
;
export
type
PreviewMode
=
"preview"
|
"code"
|
"problems"
;
export
type
PreviewMode
=
"preview"
|
"code"
|
"problems"
|
"configure"
;
// Preview Header component with preview mode toggle
export
const
PreviewHeader
=
()
=>
{
...
...
@@ -35,6 +36,7 @@ export const PreviewHeader = () => {
const
previewRef
=
useRef
<
HTMLButtonElement
>
(
null
);
const
codeRef
=
useRef
<
HTMLButtonElement
>
(
null
);
const
problemsRef
=
useRef
<
HTMLButtonElement
>
(
null
);
const
configureRef
=
useRef
<
HTMLButtonElement
>
(
null
);
const
[
indicatorStyle
,
setIndicatorStyle
]
=
useState
({
left
:
0
,
width
:
0
});
const
{
problemReport
}
=
useCheckProblems
(
selectedAppId
);
const
{
restartApp
,
refreshAppIframe
}
=
useRunApp
();
...
...
@@ -101,6 +103,9 @@ export const PreviewHeader = () => {
case
"problems"
:
targetRef
=
problemsRef
;
break
;
case
"configure"
:
targetRef
=
configureRef
;
break
;
default
:
return
;
}
...
...
@@ -146,7 +151,7 @@ export const PreviewHeader = () => {
<
button
data
-
testid=
"preview-mode-button"
ref=
{
previewRef
}
className=
"cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
className=
"cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10
hover:bg-[var(--background)]
"
onClick=
{
()
=>
selectPanel
(
"preview"
)
}
>
<
Eye
size=
{
14
}
/>
...
...
@@ -155,7 +160,7 @@ export const PreviewHeader = () => {
<
button
data
-
testid=
"problems-mode-button"
ref=
{
problemsRef
}
className=
"cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
className=
"cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10
hover:bg-[var(--background)]
"
onClick=
{
()
=>
selectPanel
(
"problems"
)
}
>
<
AlertTriangle
size=
{
14
}
/>
...
...
@@ -166,15 +171,25 @@ export const PreviewHeader = () => {
</
span
>
)
}
</
button
>
<
button
data
-
testid=
"code-mode-button"
ref=
{
codeRef
}
className=
"cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
className=
"cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10
hover:bg-[var(--background)]
"
onClick=
{
()
=>
selectPanel
(
"code"
)
}
>
<
Code
size=
{
14
}
/>
<
span
>
Code
</
span
>
</
button
>
<
button
data
-
testid=
"configure-mode-button"
ref=
{
configureRef
}
className=
"cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10 hover:bg-[var(--background)]"
onClick=
{
()
=>
selectPanel
(
"configure"
)
}
>
<
Wrench
size=
{
14
}
/>
<
span
>
Configure
</
span
>
</
button
>
</
div
>
<
div
className=
"flex items-center"
>
<
DropdownMenu
>
...
...
src/components/preview_panel/PreviewPanel.tsx
浏览文件 @
2c284d0f
...
...
@@ -9,6 +9,7 @@ import {
import
{
CodeView
}
from
"./CodeView"
;
import
{
PreviewIframe
}
from
"./PreviewIframe"
;
import
{
Problems
}
from
"./Problems"
;
import
{
ConfigurePanel
}
from
"./ConfigurePanel"
;
import
{
ChevronDown
,
ChevronUp
,
Logs
}
from
"lucide-react"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
{
PanelGroup
,
Panel
,
PanelResizeHandle
}
from
"react-resizable-panels"
;
...
...
@@ -113,6 +114,8 @@ export function PreviewPanel() {
<
PreviewIframe
key=
{
key
}
loading=
{
loading
}
/>
)
:
previewMode
===
"code"
?
(
<
CodeView
loading=
{
loading
}
app=
{
app
}
/>
)
:
previewMode
===
"configure"
?
(
<
ConfigurePanel
/>
)
:
(
<
Problems
/>
)
}
...
...
src/ipc/handlers/app_env_vars_handlers.ts
0 → 100644
浏览文件 @
2c284d0f
/**
* DO NOT USE LOGGER HERE.
* Environment variables are sensitive and should not be logged.
*/
import
{
ipcMain
}
from
"electron"
;
import
*
as
fs
from
"fs"
;
import
*
as
path
from
"path"
;
import
{
db
}
from
"../../db"
;
import
{
apps
}
from
"../../db/schema"
;
import
{
eq
}
from
"drizzle-orm"
;
import
{
getDyadAppPath
}
from
"../../paths/paths"
;
import
{
GetAppEnvVarsParams
,
SetAppEnvVarsParams
}
from
"../ipc_types"
;
import
{
parseEnvFile
,
serializeEnvFile
}
from
"../utils/app_env_var_utils"
;
export
function
registerAppEnvVarsHandlers
()
{
// Handler to get app environment variables
ipcMain
.
handle
(
"get-app-env-vars"
,
async
(
event
,
{
appId
}:
GetAppEnvVarsParams
)
=>
{
try
{
const
app
=
await
db
.
query
.
apps
.
findFirst
({
where
:
eq
(
apps
.
id
,
appId
),
});
if
(
!
app
)
{
throw
new
Error
(
"App not found"
);
}
const
appPath
=
getDyadAppPath
(
app
.
path
);
const
envFilePath
=
path
.
join
(
appPath
,
".env.local"
);
// If .env.local doesn't exist, return empty array
try
{
await
fs
.
promises
.
access
(
envFilePath
);
}
catch
{
return
[];
}
const
content
=
await
fs
.
promises
.
readFile
(
envFilePath
,
"utf8"
);
const
envVars
=
parseEnvFile
(
content
);
return
envVars
;
}
catch
(
error
)
{
console
.
error
(
"Error getting app environment variables:"
,
error
);
throw
new
Error
(
`Failed to get environment variables:
${
error
instanceof
Error
?
error
.
message
:
"Unknown error"
}
`
,
);
}
},
);
// Handler to set app environment variables
ipcMain
.
handle
(
"set-app-env-vars"
,
async
(
event
,
{
appId
,
envVars
}:
SetAppEnvVarsParams
)
=>
{
try
{
const
app
=
await
db
.
query
.
apps
.
findFirst
({
where
:
eq
(
apps
.
id
,
appId
),
});
if
(
!
app
)
{
throw
new
Error
(
"App not found"
);
}
const
appPath
=
getDyadAppPath
(
app
.
path
);
const
envFilePath
=
path
.
join
(
appPath
,
".env.local"
);
// Serialize environment variables to .env.local format
const
content
=
serializeEnvFile
(
envVars
);
// Write to .env.local file
await
fs
.
promises
.
writeFile
(
envFilePath
,
content
,
"utf8"
);
}
catch
(
error
)
{
console
.
error
(
"Error setting app environment variables:"
,
error
);
throw
new
Error
(
`Failed to set environment variables:
${
error
instanceof
Error
?
error
.
message
:
"Unknown error"
}
`
,
);
}
},
);
}
src/ipc/ipc_client.ts
浏览文件 @
2c284d0f
...
...
@@ -38,6 +38,8 @@ import type {
AppUpgrade
,
ProblemReport
,
EditAppFileReturnType
,
GetAppEnvVarsParams
,
SetAppEnvVarsParams
,
}
from
"./ipc_types"
;
import
type
{
AppChatContext
,
ProposalResult
}
from
"@/lib/schemas"
;
import
{
showError
}
from
"@/lib/toast"
;
...
...
@@ -183,6 +185,16 @@ export class IpcClient {
return
this
.
ipcRenderer
.
invoke
(
"get-app"
,
appId
);
}
public
async
getAppEnvVars
(
params
:
GetAppEnvVarsParams
,
):
Promise
<
{
key
:
string
;
value
:
string
}[]
>
{
return
this
.
ipcRenderer
.
invoke
(
"get-app-env-vars"
,
params
);
}
public
async
setAppEnvVars
(
params
:
SetAppEnvVarsParams
):
Promise
<
void
>
{
return
this
.
ipcRenderer
.
invoke
(
"set-app-env-vars"
,
params
);
}
public
async
getChat
(
chatId
:
number
):
Promise
<
Chat
>
{
try
{
const
data
=
await
this
.
ipcRenderer
.
invoke
(
"get-chat"
,
chatId
);
...
...
src/ipc/ipc_host.ts
浏览文件 @
2c284d0f
...
...
@@ -23,6 +23,7 @@ import { registerContextPathsHandlers } from "./handlers/context_paths_handlers"
import
{
registerAppUpgradeHandlers
}
from
"./handlers/app_upgrade_handlers"
;
import
{
registerCapacitorHandlers
}
from
"./handlers/capacitor_handlers"
;
import
{
registerProblemsHandlers
}
from
"./handlers/problems_handlers"
;
import
{
registerAppEnvVarsHandlers
}
from
"./handlers/app_env_vars_handlers"
;
export
function
registerIpcHandlers
()
{
// Register all IPC handlers by category
...
...
@@ -51,4 +52,5 @@ export function registerIpcHandlers() {
registerContextPathsHandlers
();
registerAppUpgradeHandlers
();
registerCapacitorHandlers
();
registerAppEnvVarsHandlers
();
}
src/ipc/ipc_types.ts
浏览文件 @
2c284d0f
...
...
@@ -252,3 +252,17 @@ export interface AppUpgrade {
export
interface
EditAppFileReturnType
{
warning
?:
string
;
}
export
interface
EnvVar
{
key
:
string
;
value
:
string
;
}
export
interface
SetAppEnvVarsParams
{
appId
:
number
;
envVars
:
EnvVar
[];
}
export
interface
GetAppEnvVarsParams
{
appId
:
number
;
}
src/ipc/utils/app_env_var_utils.ts
0 → 100644
浏览文件 @
2c284d0f
/**
* DO NOT USE LOGGER HERE.
* Environment variables are sensitive and should not be logged.
*/
import
{
EnvVar
}
from
"../ipc_types"
;
// Helper function to parse .env.local file content
export
function
parseEnvFile
(
content
:
string
):
EnvVar
[]
{
const
envVars
:
EnvVar
[]
=
[];
const
lines
=
content
.
split
(
"
\
n"
);
for
(
const
line
of
lines
)
{
const
trimmedLine
=
line
.
trim
();
// Skip empty lines and comments
if
(
!
trimmedLine
||
trimmedLine
.
startsWith
(
"#"
))
{
continue
;
}
// Parse key=value pairs
const
equalIndex
=
trimmedLine
.
indexOf
(
"="
);
if
(
equalIndex
>
0
)
{
const
key
=
trimmedLine
.
substring
(
0
,
equalIndex
).
trim
();
const
value
=
trimmedLine
.
substring
(
equalIndex
+
1
).
trim
();
// Handle quoted values with potential inline comments
let
cleanValue
=
value
;
if
(
value
.
startsWith
(
'"'
))
{
// Find the closing quote, handling escaped quotes
let
endQuoteIndex
=
-
1
;
for
(
let
i
=
1
;
i
<
value
.
length
;
i
++
)
{
if
(
value
[
i
]
===
'"'
&&
value
[
i
-
1
]
!==
"
\
\"
) {
endQuoteIndex = i;
break;
}
}
if (endQuoteIndex !== -1) {
cleanValue = value.slice(1, endQuoteIndex);
// Unescape escaped quotes
cleanValue = cleanValue.replace(/
\
\"
/g, '"
');
}
} else if (value.startsWith("'
")) {
// Find the closing quote for single quotes
const endQuoteIndex = value.indexOf("
'", 1);
if (endQuoteIndex !== -1) {
cleanValue = value.slice(1, endQuoteIndex);
}
}
// For unquoted values, keep everything as-is (including potential # symbols)
envVars.push({ key, value: cleanValue });
}
}
return envVars;
}
// Helper function to serialize environment variables to .env.local format
export function serializeEnvFile(envVars: EnvVar[]): string {
return envVars
.map(({ key, value }) => {
// Add quotes if value contains spaces or special characters
const needsQuotes = /[
\
s#"'
=&
?]
/
.
test
(
value
);
const
quotedValue
=
needsQuotes
?
`"
${
value
.
replace
(
/"/g
,
'
\\
"'
)}
"`
:
value
;
return
`
${
key
}
=
${
quotedValue
}
`
;
})
.
join
(
"
\
n"
);
}
src/preload.ts
浏览文件 @
2c284d0f
...
...
@@ -26,6 +26,8 @@ const validInvokeChannels = [
"get-chat-logs"
,
"list-apps"
,
"get-app"
,
"get-app-env-vars"
,
"set-app-env-vars"
,
"edit-app-file"
,
"read-app-file"
,
"run-app"
,
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论