project-bootstrap.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { access, mkdir, readFile, writeFile } from "node:fs/promises";
  2. import { basename, join } from "node:path";
  3. import { GLOBAL_ENV_PATH } from "./utils.js";
  4. export interface ProjectBootstrapOptions {
  5. readonly language?: "zh" | "en";
  6. readonly overwriteSupportFiles?: boolean;
  7. }
  8. async function hasGlobalConfig(): Promise<boolean> {
  9. try {
  10. const content = await readFile(GLOBAL_ENV_PATH, "utf-8");
  11. return content.includes("INKOS_LLM_API_KEY=") && !content.includes("your-api-key-here");
  12. } catch {
  13. return false;
  14. }
  15. }
  16. async function exists(path: string): Promise<boolean> {
  17. try {
  18. await access(path);
  19. return true;
  20. } catch {
  21. return false;
  22. }
  23. }
  24. async function writeMaybe(path: string, content: string, overwrite: boolean): Promise<void> {
  25. if (!overwrite && await exists(path)) {
  26. return;
  27. }
  28. await writeFile(path, content, "utf-8");
  29. }
  30. function buildProjectConfig(projectDir: string, language: "zh" | "en") {
  31. return {
  32. name: basename(projectDir),
  33. version: "0.1.0" as const,
  34. language,
  35. llm: {
  36. provider: "openai" as const,
  37. service: "custom",
  38. configSource: "studio" as const,
  39. baseUrl: "",
  40. model: "",
  41. apiFormat: "chat" as const,
  42. stream: true,
  43. },
  44. notify: [],
  45. inputGovernanceMode: "v2" as const,
  46. daemon: {
  47. schedule: {
  48. radarCron: "0 */6 * * *",
  49. writeCron: "*/15 * * * *",
  50. },
  51. maxConcurrentBooks: 3,
  52. },
  53. };
  54. }
  55. function buildProjectEnvTemplate(globalConfigured: boolean): string {
  56. if (globalConfigured) {
  57. return [
  58. "# Project-level LLM overrides (optional)",
  59. "# Global config at ~/.inkos/.env will be used by default.",
  60. "# Switch Studio to '使用 Studio 配置' if you want per-project service settings.",
  61. "# Uncomment below to override for this project only:",
  62. "# INKOS_LLM_PROVIDER=openai",
  63. "# INKOS_LLM_BASE_URL=",
  64. "# INKOS_LLM_API_KEY=",
  65. "# INKOS_LLM_MODEL=",
  66. "",
  67. "# Web search (optional):",
  68. "# TAVILY_API_KEY=tvly-xxxxx",
  69. "",
  70. ].join("\n");
  71. }
  72. return [
  73. "# Optional project-level LLM overrides",
  74. "# Studio can manage provider / model / key without editing this file.",
  75. "# Uncomment only if you want this directory to force env-based config:",
  76. "# INKOS_LLM_PROVIDER=openai",
  77. "# INKOS_LLM_BASE_URL=",
  78. "# INKOS_LLM_API_KEY=",
  79. "# INKOS_LLM_MODEL=",
  80. "# INKOS_LLM_API_FORMAT=chat",
  81. "# INKOS_LLM_STREAM=true",
  82. "",
  83. "# Web search (optional):",
  84. "# TAVILY_API_KEY=tvly-xxxxx",
  85. "",
  86. ].join("\n");
  87. }
  88. export async function initializeProjectDirectory(
  89. projectDir: string,
  90. options: ProjectBootstrapOptions = {},
  91. ): Promise<void> {
  92. const language = options.language ?? "zh";
  93. const overwriteSupportFiles = options.overwriteSupportFiles ?? true;
  94. const configPath = join(projectDir, "inkos.json");
  95. if (await exists(configPath)) {
  96. throw new Error(`inkos.json already exists in ${projectDir}. Use a different directory or delete the existing project.`);
  97. }
  98. await mkdir(projectDir, { recursive: true });
  99. await mkdir(join(projectDir, "books"), { recursive: true });
  100. await mkdir(join(projectDir, "radar"), { recursive: true });
  101. await writeFile(
  102. configPath,
  103. JSON.stringify(buildProjectConfig(projectDir, language), null, 2),
  104. "utf-8",
  105. );
  106. const globalConfigured = await hasGlobalConfig();
  107. await Promise.all([
  108. writeMaybe(join(projectDir, ".env"), buildProjectEnvTemplate(globalConfigured), overwriteSupportFiles),
  109. writeMaybe(join(projectDir, ".gitignore"), [".env", "node_modules/", ".DS_Store"].join("\n"), overwriteSupportFiles),
  110. writeMaybe(join(projectDir, ".nvmrc"), "22\n", overwriteSupportFiles),
  111. writeMaybe(join(projectDir, ".node-version"), "22\n", overwriteSupportFiles),
  112. ]);
  113. }
  114. export async function ensureProjectDirectoryInitialized(
  115. projectDir: string,
  116. options: Omit<ProjectBootstrapOptions, "overwriteSupportFiles"> = {},
  117. ): Promise<boolean> {
  118. const configPath = join(projectDir, "inkos.json");
  119. if (await exists(configPath)) {
  120. return false;
  121. }
  122. await initializeProjectDirectory(projectDir, {
  123. language: options.language,
  124. overwriteSupportFiles: false,
  125. });
  126. return true;
  127. }