init.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { Command } from "commander";
  2. import { access, readFile, writeFile, mkdir } from "node:fs/promises";
  3. import { join, basename, resolve } from "node:path";
  4. import { log, logError, GLOBAL_ENV_PATH } from "../utils.js";
  5. async function hasGlobalConfig(): Promise<boolean> {
  6. try {
  7. const content = await readFile(GLOBAL_ENV_PATH, "utf-8");
  8. return content.includes("INKOS_LLM_API_KEY=") && !content.includes("your-api-key-here");
  9. } catch {
  10. return false;
  11. }
  12. }
  13. export const initCommand = new Command("init")
  14. .description("Initialize an InkOS project (current directory by default)")
  15. .argument("[name]", "Project name (creates subdirectory). Omit to init current directory.")
  16. .option("--lang <language>", "Default writing language: zh (Chinese) or en (English)", "zh")
  17. .action(async (name: string | undefined, opts: { lang?: string }) => {
  18. const projectDir = name ? resolve(process.cwd(), name) : process.cwd();
  19. const projectName = basename(projectDir);
  20. try {
  21. await mkdir(projectDir, { recursive: true });
  22. // Check if inkos.json already exists
  23. const configPath = join(projectDir, "inkos.json");
  24. try {
  25. await access(configPath);
  26. throw new Error(`inkos.json already exists in ${projectDir}. Use a different directory or delete the existing project.`);
  27. } catch (e) {
  28. if (e instanceof Error && e.message.includes("already exists")) throw e;
  29. // File doesn't exist, good
  30. }
  31. await mkdir(join(projectDir, "books"), { recursive: true });
  32. await mkdir(join(projectDir, "radar"), { recursive: true });
  33. const config = {
  34. name: projectName,
  35. version: "0.1.0",
  36. language: opts.lang ?? "zh",
  37. llm: {
  38. provider: process.env.INKOS_LLM_PROVIDER ?? "openai",
  39. baseUrl: process.env.INKOS_LLM_BASE_URL ?? "",
  40. model: process.env.INKOS_LLM_MODEL ?? "",
  41. },
  42. notify: [],
  43. daemon: {
  44. schedule: {
  45. radarCron: "0 */6 * * *",
  46. writeCron: "*/15 * * * *",
  47. },
  48. maxConcurrentBooks: 3,
  49. },
  50. };
  51. await writeFile(
  52. join(projectDir, "inkos.json"),
  53. JSON.stringify(config, null, 2),
  54. "utf-8",
  55. );
  56. await Promise.all([
  57. writeFile(join(projectDir, ".nvmrc"), "22\n", "utf-8"),
  58. writeFile(join(projectDir, ".node-version"), "22\n", "utf-8"),
  59. ]);
  60. const global = await hasGlobalConfig();
  61. if (global) {
  62. await writeFile(
  63. join(projectDir, ".env"),
  64. [
  65. "# Project-level LLM overrides (optional)",
  66. "# Global config at ~/.inkos/.env will be used by default.",
  67. "# Uncomment below to override for this project only:",
  68. "# INKOS_LLM_PROVIDER=openai",
  69. "# INKOS_LLM_BASE_URL=",
  70. "# INKOS_LLM_API_KEY=",
  71. "# INKOS_LLM_MODEL=",
  72. "",
  73. "# Web search (optional):",
  74. "# TAVILY_API_KEY=tvly-xxxxx",
  75. ].join("\n"),
  76. "utf-8",
  77. );
  78. } else {
  79. await writeFile(
  80. join(projectDir, ".env"),
  81. [
  82. "# LLM Configuration",
  83. "# Tip: Run 'inkos config set-global' to set once for all projects.",
  84. "# Provider: openai (OpenAI / compatible proxy), anthropic (Anthropic native)",
  85. "INKOS_LLM_PROVIDER=openai",
  86. "INKOS_LLM_BASE_URL=",
  87. "INKOS_LLM_API_KEY=",
  88. "INKOS_LLM_MODEL=",
  89. "",
  90. "# Optional parameters (defaults shown):",
  91. "# INKOS_LLM_TEMPERATURE=0.7",
  92. "# INKOS_LLM_MAX_TOKENS=8192",
  93. "# INKOS_LLM_THINKING_BUDGET=0 # Anthropic extended thinking budget",
  94. "# INKOS_LLM_API_FORMAT=chat # chat (default) or responses (OpenAI Responses API)",
  95. "",
  96. "# Web search (optional, for auditor era-research):",
  97. "# TAVILY_API_KEY=tvly-xxxxx # Free at tavily.com (1000 searches/month)",
  98. "",
  99. "# Anthropic example:",
  100. "# INKOS_LLM_PROVIDER=anthropic",
  101. "# INKOS_LLM_PROVIDER=anthropic",
  102. "# INKOS_LLM_BASE_URL=",
  103. "# INKOS_LLM_MODEL=",
  104. ].join("\n"),
  105. "utf-8",
  106. );
  107. }
  108. await writeFile(
  109. join(projectDir, ".gitignore"),
  110. [".env", "node_modules/", ".DS_Store"].join("\n"),
  111. "utf-8",
  112. );
  113. log(`Project initialized at ${projectDir}`);
  114. log("");
  115. const isEnglish = (opts.lang ?? "zh") === "en";
  116. const exampleCreate = isEnglish
  117. ? " inkos book create --title 'My Novel' --genre progression --platform royalroad --lang en"
  118. : " inkos book create --title '我的小说' --genre xuanhuan --platform tomato";
  119. if (global) {
  120. log("Global LLM config detected. Ready to go!");
  121. log("");
  122. log("Next steps:");
  123. if (name) log(` cd ${name}`);
  124. log(exampleCreate);
  125. } else {
  126. log("Next steps:");
  127. if (name) log(` cd ${name}`);
  128. log(" # Option 1: Set global config (recommended, one-time):");
  129. log(" inkos config set-global --provider openai --base-url <your-api-url> --api-key <your-key> --model <your-model>");
  130. log(" # Option 2: Edit .env for this project only");
  131. log("");
  132. log(exampleCreate);
  133. }
  134. log(" inkos write next <book-id>");
  135. } catch (e) {
  136. logError(`Failed to initialize project: ${e}`);
  137. process.exit(1);
  138. }
  139. });