@01.software/init
Advanced tools
| #!/usr/bin/env node | ||
| // src/file-ops.ts | ||
| var envLineRegexCache = /* @__PURE__ */ new Map(); | ||
| function envLineRegex(name) { | ||
| let re = envLineRegexCache.get(name); | ||
| if (!re) { | ||
| const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | ||
| re = new RegExp(`^${escaped}=(.*)$`, "m"); | ||
| envLineRegexCache.set(name, re); | ||
| } | ||
| return re; | ||
| } | ||
| function readEnvValue(content, name) { | ||
| const m = content.match(envLineRegex(name)); | ||
| return m ? m[1] : null; | ||
| } | ||
| function setEnvValue(content, name, value) { | ||
| const re = envLineRegex(name); | ||
| if (re.test(content)) return content.replace(re, `${name}=${value}`); | ||
| const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n"; | ||
| return content + sep + `${name}=${value} | ||
| `; | ||
| } | ||
| var TOML_SECTION_PREFIX = "[mcp_servers.01software"; | ||
| function extractTomlApiKey(content) { | ||
| const headerIdx = content.indexOf(`${TOML_SECTION_PREFIX}.headers]`); | ||
| if (headerIdx < 0) return null; | ||
| const slice = content.slice(headerIdx); | ||
| const m = slice.match(/^x-api-key\s*=\s*"((?:[^"\\]|\\.)*)"/m); | ||
| if (!m) return null; | ||
| return m[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"); | ||
| } | ||
| function replaceTomlMcpSection(content, newSection) { | ||
| const lines = content.split("\n"); | ||
| const kept = []; | ||
| let inOurSection = false; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (trimmed.startsWith(TOML_SECTION_PREFIX)) { | ||
| inOurSection = true; | ||
| continue; | ||
| } | ||
| if (inOurSection && /^\[[^\[]/.test(trimmed)) { | ||
| inOurSection = false; | ||
| } | ||
| if (!inOurSection) kept.push(line); | ||
| } | ||
| let result = kept.join("\n").replace(/\n+$/, ""); | ||
| if (result.length > 0) result += "\n"; | ||
| result += newSection.startsWith("\n") ? newSection : "\n" + newSection; | ||
| return result; | ||
| } | ||
| export { | ||
| readEnvValue, | ||
| setEnvValue, | ||
| extractTomlApiKey, | ||
| replaceTomlMcpSection | ||
| }; | ||
| //# sourceMappingURL=chunk-VOEXMD2S.js.map |
| {"version":3,"sources":["../src/file-ops.ts"],"sourcesContent":["// Pure helpers for in-place file content manipulation. Kept side-effect-free\n// so they can be unit-tested without touching the filesystem or prompts.\n\n// ── .env merge ───────────────────────────────────────────────────────\n\nconst envLineRegexCache = new Map<string, RegExp>()\n\nfunction envLineRegex(name: string): RegExp {\n let re = envLineRegexCache.get(name)\n if (!re) {\n const escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n re = new RegExp(`^${escaped}=(.*)$`, 'm')\n envLineRegexCache.set(name, re)\n }\n return re\n}\n\nexport function readEnvValue(content: string, name: string): string | null {\n const m = content.match(envLineRegex(name))\n return m ? m[1] : null\n}\n\nexport function setEnvValue(content: string, name: string, value: string): string {\n const re = envLineRegex(name)\n if (re.test(content)) return content.replace(re, `${name}=${value}`)\n const sep = content.length === 0 || content.endsWith('\\n') ? '' : '\\n'\n return content + sep + `${name}=${value}\\n`\n}\n\n// ── Codex TOML manipulation ──────────────────────────────────────────\n\nconst TOML_SECTION_PREFIX = '[mcp_servers.01software'\n\n/** Pulls the current `x-api-key` value from `[mcp_servers.01software.headers]`.\n * Returns null if the section or key is absent. */\nexport function extractTomlApiKey(content: string): string | null {\n const headerIdx = content.indexOf(`${TOML_SECTION_PREFIX}.headers]`)\n if (headerIdx < 0) return null\n const slice = content.slice(headerIdx)\n const m = slice.match(/^x-api-key\\s*=\\s*\"((?:[^\"\\\\]|\\\\.)*)\"/m)\n if (!m) return null\n // Reverse the same escaping used when writing (escape backslash + quote)\n return m[1].replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\')\n}\n\n/** Removes the existing `[mcp_servers.01software]` block (and its `.headers`\n * sub-block) from a TOML document, then appends the provided section. */\nexport function replaceTomlMcpSection(content: string, newSection: string): string {\n const lines = content.split('\\n')\n const kept: string[] = []\n let inOurSection = false\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed.startsWith(TOML_SECTION_PREFIX)) {\n inOurSection = true\n continue\n }\n // Any other top-level header ends our section\n if (inOurSection && /^\\[[^\\[]/.test(trimmed)) {\n inOurSection = false\n }\n if (!inOurSection) kept.push(line)\n }\n let result = kept.join('\\n').replace(/\\n+$/, '')\n if (result.length > 0) result += '\\n'\n // newSection already starts with a blank line; keep it that way\n result += newSection.startsWith('\\n') ? newSection : '\\n' + newSection\n return result\n}\n"],"mappings":";;;AAKA,IAAM,oBAAoB,oBAAI,IAAoB;AAElD,SAAS,aAAa,MAAsB;AAC1C,MAAI,KAAK,kBAAkB,IAAI,IAAI;AACnC,MAAI,CAAC,IAAI;AACP,UAAM,UAAU,KAAK,QAAQ,uBAAuB,MAAM;AAC1D,SAAK,IAAI,OAAO,IAAI,OAAO,UAAU,GAAG;AACxC,sBAAkB,IAAI,MAAM,EAAE;AAAA,EAChC;AACA,SAAO;AACT;AAEO,SAAS,aAAa,SAAiB,MAA6B;AACzE,QAAM,IAAI,QAAQ,MAAM,aAAa,IAAI,CAAC;AAC1C,SAAO,IAAI,EAAE,CAAC,IAAI;AACpB;AAEO,SAAS,YAAY,SAAiB,MAAc,OAAuB;AAChF,QAAM,KAAK,aAAa,IAAI;AAC5B,MAAI,GAAG,KAAK,OAAO,EAAG,QAAO,QAAQ,QAAQ,IAAI,GAAG,IAAI,IAAI,KAAK,EAAE;AACnE,QAAM,MAAM,QAAQ,WAAW,KAAK,QAAQ,SAAS,IAAI,IAAI,KAAK;AAClE,SAAO,UAAU,MAAM,GAAG,IAAI,IAAI,KAAK;AAAA;AACzC;AAIA,IAAM,sBAAsB;AAIrB,SAAS,kBAAkB,SAAgC;AAChE,QAAM,YAAY,QAAQ,QAAQ,GAAG,mBAAmB,WAAW;AACnE,MAAI,YAAY,EAAG,QAAO;AAC1B,QAAM,QAAQ,QAAQ,MAAM,SAAS;AACrC,QAAM,IAAI,MAAM,MAAM,uCAAuC;AAC7D,MAAI,CAAC,EAAG,QAAO;AAEf,SAAO,EAAE,CAAC,EAAE,QAAQ,QAAQ,GAAG,EAAE,QAAQ,SAAS,IAAI;AACxD;AAIO,SAAS,sBAAsB,SAAiB,YAA4B;AACjF,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,OAAiB,CAAC;AACxB,MAAI,eAAe;AACnB,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,QAAQ,WAAW,mBAAmB,GAAG;AAC3C,qBAAe;AACf;AAAA,IACF;AAEA,QAAI,gBAAgB,WAAW,KAAK,OAAO,GAAG;AAC5C,qBAAe;AAAA,IACjB;AACA,QAAI,CAAC,aAAc,MAAK,KAAK,IAAI;AAAA,EACnC;AACA,MAAI,SAAS,KAAK,KAAK,IAAI,EAAE,QAAQ,QAAQ,EAAE;AAC/C,MAAI,OAAO,SAAS,EAAG,WAAU;AAEjC,YAAU,WAAW,WAAW,IAAI,IAAI,aAAa,OAAO;AAC5D,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| extractTomlApiKey, | ||
| readEnvValue, | ||
| replaceTomlMcpSection, | ||
| setEnvValue | ||
| } from "./chunk-VOEXMD2S.js"; | ||
| export { | ||
| extractTomlApiKey, | ||
| readEnvValue, | ||
| replaceTomlMcpSection, | ||
| setEnvValue | ||
| }; | ||
| //# sourceMappingURL=file-ops.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
+302
-98
@@ -8,2 +8,8 @@ #!/usr/bin/env node | ||
| import { | ||
| extractTomlApiKey, | ||
| readEnvValue, | ||
| replaceTomlMcpSection, | ||
| setEnvValue | ||
| } from "./chunk-VOEXMD2S.js"; | ||
| import { | ||
| CODEX_MCP_SECTION_MARKER, | ||
@@ -30,2 +36,3 @@ getClientTemplate, | ||
| let hasSdk = false; | ||
| let hasReactQuery = false; | ||
| let parseError = false; | ||
@@ -37,3 +44,11 @@ if (hasPackageJson) { | ||
| } catch { | ||
| return { hasPackageJson: true, parseError: true, env: "node", packageManager: null, hasSdk: false, srcDir: false }; | ||
| return { | ||
| hasPackageJson: true, | ||
| parseError: true, | ||
| env: "node", | ||
| packageManager: null, | ||
| hasSdk: false, | ||
| hasReactQuery: false, | ||
| srcDir: false | ||
| }; | ||
| } | ||
@@ -45,2 +60,3 @@ const deps = { | ||
| hasSdk = "@01.software/sdk" in deps; | ||
| hasReactQuery = "@tanstack/react-query" in deps; | ||
| if ("next" in deps) { | ||
@@ -75,3 +91,3 @@ env = "nextjs"; | ||
| const srcDir = env === "nextjs" ? fs.existsSync(path.join(cwd, "src", "app")) : fs.existsSync(path.join(cwd, "src")); | ||
| return { hasPackageJson, parseError, env, packageManager, hasSdk, srcDir }; | ||
| return { hasPackageJson, parseError, env, packageManager, hasSdk, hasReactQuery, srcDir }; | ||
| } | ||
@@ -267,2 +283,3 @@ function needsClient(env) { | ||
| import pc2 from "picocolors"; | ||
| import prompts2 from "prompts"; | ||
@@ -448,2 +465,3 @@ // src/browser-auth.ts | ||
| var SECRET_KEY_ENV_VAR = "SOFTWARE_SECRET_KEY"; | ||
| var API_KEY_PLACEHOLDER = "YOUR_API_KEY"; | ||
| async function init(cwd, info, answers) { | ||
@@ -457,52 +475,47 @@ const { packageManager, srcDir } = info; | ||
| const wantsReactQuery = needsReactQuery(env); | ||
| const deps = ["@01.software/sdk"]; | ||
| if (wantsReactQuery) deps.push("@tanstack/react-query"); | ||
| const addCmd = buildAddCmd(packageManager, hasPnpmWorkspace(cwd), deps); | ||
| let installFailed = false; | ||
| console.log(pc2.dim(` Installing ${deps.join(" and ")}...`)); | ||
| const wsPatched = packageManager === "pnpm" && patchPnpmWorkspace(cwd); | ||
| try { | ||
| execSync(addCmd, { cwd, stdio: "pipe" }); | ||
| console.log(pc2.green(" Installed"), deps.join(", ")); | ||
| } catch (error) { | ||
| installFailed = true; | ||
| const err = error; | ||
| const msg = String(err.stderr || "").trim() || String(err.stdout || "").trim() || String(error); | ||
| console.log(pc2.yellow(" Install failed \u2014 continuing with scaffolding")); | ||
| const firstLines = msg.split("\n").slice(0, 3).map((l) => ` ${l}`).join("\n"); | ||
| if (firstLines) console.log(pc2.dim(firstLines)); | ||
| console.log(pc2.dim(` Run manually: ${addCmd}`)); | ||
| } finally { | ||
| if (wsPatched) restorePnpmWorkspace(cwd); | ||
| const plan = await planConflictsAndEnv(cwd, baseDir, env, answers); | ||
| const installResult = installDeps( | ||
| cwd, | ||
| packageManager, | ||
| info.hasSdk, | ||
| info.hasReactQuery, | ||
| wantsReactQuery | ||
| ); | ||
| if (wantsClient || wantsReactQuery || wantsServer) { | ||
| fs2.mkdirSync(path2.join(baseDir, "lib", "software"), { recursive: true }); | ||
| } | ||
| const libDir = path2.join(baseDir, "lib", "software"); | ||
| fs2.mkdirSync(libDir, { recursive: true }); | ||
| if (wantsClient) { | ||
| writeFileIfAbsent( | ||
| await writeFileWithPolicy( | ||
| cwd, | ||
| path2.join(libDir, "client.ts"), | ||
| getClientTemplate(env, publishableKeyEnvVar) | ||
| getClientTemplate(env, publishableKeyEnvVar), | ||
| plan.policy | ||
| ); | ||
| } | ||
| if (wantsReactQuery) { | ||
| writeFileIfAbsent( | ||
| await writeFileWithPolicy( | ||
| cwd, | ||
| path2.join(libDir, "query-provider.tsx"), | ||
| getQueryProviderTemplate(env) | ||
| getQueryProviderTemplate(env), | ||
| plan.policy | ||
| ); | ||
| } | ||
| if (wantsServer) { | ||
| writeFileIfAbsent( | ||
| await writeFileWithPolicy( | ||
| cwd, | ||
| path2.join(libDir, "server.ts"), | ||
| getServerTemplate(env, publishableKeyEnvVar, SECRET_KEY_ENV_VAR) | ||
| getServerTemplate(env, publishableKeyEnvVar, SECRET_KEY_ENV_VAR), | ||
| plan.policy | ||
| ); | ||
| } | ||
| if (env !== "vanilla" && env !== "edge" && answers.authMethod !== "browser") { | ||
| writeEnv( | ||
| if (plan.envFile && answers.authMethod !== "browser") { | ||
| await writeEnv( | ||
| cwd, | ||
| plan.envFile, | ||
| answers.publishableKey || "", | ||
| answers.secretKey || "", | ||
| publishableKeyEnvVar, | ||
| wantsServer ? SECRET_KEY_ENV_VAR : null | ||
| wantsServer ? SECRET_KEY_ENV_VAR : null, | ||
| plan.policy | ||
| ); | ||
@@ -520,5 +533,6 @@ } | ||
| tenantName = creds.tenantName; | ||
| if (env !== "vanilla" && env !== "edge" && publishableKey) { | ||
| writeEnv( | ||
| if (plan.envFile && publishableKey) { | ||
| await writeEnv( | ||
| cwd, | ||
| plan.envFile, | ||
| publishableKey, | ||
@@ -528,2 +542,3 @@ secretKey, | ||
| wantsServer ? SECRET_KEY_ENV_VAR : null, | ||
| "overwrite", | ||
| true | ||
@@ -540,13 +555,45 @@ ); | ||
| if (answers.aiTools.length > 0) { | ||
| const apiKey = secretKey || "YOUR_API_KEY"; | ||
| const apiKey = secretKey || API_KEY_PLACEHOLDER; | ||
| for (const tool of answers.aiTools) { | ||
| writeMcpConfig(tool, cwd, apiKey); | ||
| await writeMcpConfig(tool, cwd, apiKey, plan.policy); | ||
| } | ||
| addToGitignore(cwd, answers.aiTools); | ||
| if (answers.aiTools.includes("claude")) { | ||
| await writeClaudeDocs(cwd, publishableKey, secretKey, tenantName); | ||
| await writeClaudeDocs(cwd, publishableKey, secretKey, tenantName, plan.policy); | ||
| } | ||
| } | ||
| return { installFailed, installCmd: addCmd }; | ||
| return installResult; | ||
| } | ||
| function installDeps(cwd, pm, hasSdk, hasReactQuery, wantsReactQuery) { | ||
| const allDeps = [ | ||
| { name: "@01.software/sdk", installed: hasSdk, needed: true }, | ||
| { name: "@tanstack/react-query", installed: hasReactQuery, needed: wantsReactQuery } | ||
| ]; | ||
| const fullList = allDeps.filter((d) => d.needed).map((d) => d.name); | ||
| const toInstall = allDeps.filter((d) => d.needed && !d.installed).map((d) => d.name); | ||
| const fullCmd = buildAddCmd(pm, hasPnpmWorkspace(cwd), fullList); | ||
| if (toInstall.length === 0) { | ||
| console.log(pc2.dim(` Dependencies already installed: ${fullList.join(", ")}`)); | ||
| return { installFailed: false, installSkipped: true, installCmd: fullCmd }; | ||
| } | ||
| const addCmd = buildAddCmd(pm, hasPnpmWorkspace(cwd), toInstall); | ||
| console.log(pc2.dim(` Installing ${toInstall.join(" and ")}...`)); | ||
| const wsPatched = pm === "pnpm" && patchPnpmWorkspace(cwd); | ||
| let installFailed = false; | ||
| try { | ||
| execSync(addCmd, { cwd, stdio: "pipe" }); | ||
| console.log(pc2.green(" Installed"), toInstall.join(", ")); | ||
| } catch (error) { | ||
| installFailed = true; | ||
| const err = error; | ||
| const msg = String(err.stderr || "").trim() || String(err.stdout || "").trim() || String(error); | ||
| console.log(pc2.yellow(" Install failed \u2014 continuing with scaffolding")); | ||
| const firstLines = msg.split("\n").slice(0, 3).map((l) => ` ${l}`).join("\n"); | ||
| if (firstLines) console.log(pc2.dim(firstLines)); | ||
| console.log(pc2.dim(` Run manually: ${addCmd}`)); | ||
| } finally { | ||
| if (wsPatched) restorePnpmWorkspace(cwd); | ||
| } | ||
| return { installFailed, installSkipped: false, installCmd: addCmd }; | ||
| } | ||
| function buildAddCmd(pm, hasPnpmWs, deps) { | ||
@@ -565,30 +612,151 @@ const pkgs = deps.join(" "); | ||
| } | ||
| function writeFileIfAbsent(cwd, filePath, content) { | ||
| if (fs2.existsSync(filePath)) { | ||
| console.log(pc2.yellow(" Skipped"), relativePath(cwd, filePath), pc2.dim("(already exists)")); | ||
| async function planConflictsAndEnv(cwd, baseDir, env, answers) { | ||
| const candidates = []; | ||
| const libDir = path2.join(baseDir, "lib", "software"); | ||
| if (needsClient(env)) candidates.push(path2.join(libDir, "client.ts")); | ||
| if (needsReactQuery(env)) candidates.push(path2.join(libDir, "query-provider.tsx")); | ||
| if (needsServer(env)) candidates.push(path2.join(libDir, "server.ts")); | ||
| if (answers.aiTools.includes("claude")) { | ||
| for (const { dirName } of getSkillFiles()) { | ||
| candidates.push(path2.join(cwd, ".claude", "skills", dirName, "SKILL.md")); | ||
| } | ||
| } | ||
| const conflicts = candidates.filter((p) => fs2.existsSync(p)); | ||
| let policy = "skip"; | ||
| if (conflicts.length > 0) { | ||
| console.log(pc2.yellow(` ${conflicts.length} file(s) already exist:`)); | ||
| for (const c of conflicts) console.log(pc2.dim(` ${path2.relative(cwd, c)}`)); | ||
| const { selected } = await prompts2({ | ||
| type: "select", | ||
| name: "selected", | ||
| message: "How should I handle existing files?", | ||
| choices: [ | ||
| { title: "Keep existing (skip)", value: "skip" }, | ||
| { title: "Overwrite all", value: "overwrite" }, | ||
| { title: "Ask for each", value: "ask" } | ||
| ], | ||
| initial: 0 | ||
| }); | ||
| policy = selected ?? "skip"; | ||
| } | ||
| const envFile = env === "vanilla" || env === "edge" ? "" : await pickEnvFile(cwd, env); | ||
| return { policy, envFile }; | ||
| } | ||
| async function pickEnvFile(cwd, env) { | ||
| const candidates = [".env.local", ".env", ".env.development"]; | ||
| const existing = candidates.filter((f) => fs2.existsSync(path2.join(cwd, f))); | ||
| const preferred = env === "nextjs" ? ".env.local" : ".env"; | ||
| if (existing.length === 0) return preferred; | ||
| if (existing.length === 1 && existing[0] === preferred) return existing[0]; | ||
| const options = Array.from(/* @__PURE__ */ new Set([...existing, preferred])); | ||
| const choices = options.map((f) => ({ | ||
| title: f, | ||
| description: existing.includes(f) ? "exists" : "create", | ||
| value: f | ||
| })); | ||
| const initial = Math.max( | ||
| 0, | ||
| choices.findIndex((c) => c.value === preferred) | ||
| ); | ||
| const { file } = await prompts2({ | ||
| type: "select", | ||
| name: "file", | ||
| message: "Which env file should I write API keys to?", | ||
| choices, | ||
| initial | ||
| }); | ||
| return file ?? preferred; | ||
| } | ||
| async function writeFileWithPolicy(cwd, filePath, content, policy) { | ||
| const rel = path2.relative(cwd, filePath); | ||
| if (!fs2.existsSync(filePath)) { | ||
| fs2.mkdirSync(path2.dirname(filePath), { recursive: true }); | ||
| fs2.writeFileSync(filePath, content); | ||
| console.log(pc2.green(" Created"), rel); | ||
| return; | ||
| } | ||
| fs2.writeFileSync(filePath, content); | ||
| console.log(pc2.green(" Created"), relativePath(cwd, filePath)); | ||
| const existing = fs2.readFileSync(filePath, "utf-8"); | ||
| if (existing === content) { | ||
| console.log(pc2.dim(" Unchanged"), rel); | ||
| return; | ||
| } | ||
| let shouldWrite = false; | ||
| if (policy === "overwrite") { | ||
| shouldWrite = true; | ||
| } else if (policy === "ask") { | ||
| const { confirm } = await prompts2({ | ||
| type: "confirm", | ||
| name: "confirm", | ||
| message: `Overwrite ${rel}?`, | ||
| initial: false | ||
| }); | ||
| shouldWrite = !!confirm; | ||
| } | ||
| if (shouldWrite) { | ||
| fs2.writeFileSync(filePath, content); | ||
| console.log(pc2.green(" Overwrote"), rel); | ||
| } else { | ||
| console.log(pc2.yellow(" Skipped"), rel, pc2.dim("(already exists)")); | ||
| } | ||
| } | ||
| function writeEnv(cwd, publishableKey, secretKey, publishableKeyEnvVar, secretKeyEnvVar, afterBrowserAuth = false) { | ||
| const envPath = path2.join(cwd, ".env"); | ||
| const envContent = getEnvContent(publishableKey, secretKey, publishableKeyEnvVar, secretKeyEnvVar); | ||
| if (fs2.existsSync(envPath)) { | ||
| const existing = fs2.readFileSync(envPath, "utf-8"); | ||
| if (existing.includes(publishableKeyEnvVar)) { | ||
| if (!afterBrowserAuth) { | ||
| console.log(pc2.yellow(" Skipped"), ".env", pc2.dim("(keys already present)")); | ||
| async function writeEnv(cwd, envFile, publishableKey, secretKey, publishableKeyEnvVar, secretKeyEnvVar, policy, fromBrowserAuth = false) { | ||
| const envPath = path2.join(cwd, envFile); | ||
| const targets = [ | ||
| { name: publishableKeyEnvVar, value: publishableKey } | ||
| ]; | ||
| if (secretKeyEnvVar) { | ||
| targets.push({ name: secretKeyEnvVar, value: secretKey }); | ||
| } | ||
| if (!fs2.existsSync(envPath)) { | ||
| const initial = getEnvContent(publishableKey, secretKey, publishableKeyEnvVar, secretKeyEnvVar); | ||
| fs2.writeFileSync(envPath, initial.trimStart()); | ||
| console.log(pc2.green(" Created"), envFile); | ||
| return; | ||
| } | ||
| let content = fs2.readFileSync(envPath, "utf-8"); | ||
| let modified = false; | ||
| let appendedHeader = false; | ||
| const headerAlreadyPresent = targets.some( | ||
| (t) => readEnvValue(content, t.name) !== null | ||
| ); | ||
| for (const { name, value } of targets) { | ||
| const existing = readEnvValue(content, name); | ||
| if (existing === null) { | ||
| if (!headerAlreadyPresent && !appendedHeader) { | ||
| if (content.length > 0 && !content.endsWith("\n")) content += "\n"; | ||
| content += "\n# 01.software\n"; | ||
| appendedHeader = true; | ||
| } | ||
| return; | ||
| content = setEnvValue(content, name, value); | ||
| modified = true; | ||
| continue; | ||
| } | ||
| fs2.appendFileSync(envPath, envContent); | ||
| if (existing === value) continue; | ||
| if (!value) continue; | ||
| let shouldOverwrite = false; | ||
| if (policy === "overwrite") { | ||
| shouldOverwrite = true; | ||
| } else if (policy === "ask") { | ||
| const { confirm } = await prompts2({ | ||
| type: "confirm", | ||
| name: "confirm", | ||
| message: `${name} already set in ${envFile}. Overwrite?`, | ||
| initial: fromBrowserAuth | ||
| }); | ||
| shouldOverwrite = !!confirm; | ||
| } | ||
| if (shouldOverwrite) { | ||
| content = setEnvValue(content, name, value); | ||
| modified = true; | ||
| } | ||
| } | ||
| if (modified) { | ||
| fs2.writeFileSync(envPath, content); | ||
| console.log( | ||
| pc2.green(" Updated"), | ||
| ".env", | ||
| afterBrowserAuth ? pc2.dim("(added API keys)") : "" | ||
| envFile, | ||
| fromBrowserAuth ? pc2.dim("(API keys)") : "" | ||
| ); | ||
| } else { | ||
| fs2.writeFileSync(envPath, envContent.trimStart()); | ||
| console.log(pc2.green(" Created"), ".env"); | ||
| console.log(pc2.dim(" Unchanged"), envFile); | ||
| } | ||
@@ -637,3 +805,3 @@ } | ||
| } | ||
| function writeMcpConfig(tool, cwd, apiKey) { | ||
| async function writeMcpConfig(tool, cwd, apiKey, policy) { | ||
| const loc = resolveMcpLocation(tool, cwd); | ||
@@ -646,43 +814,83 @@ if (!loc) { | ||
| if (loc.kind === "json") { | ||
| writeJsonMcp(loc, apiKey); | ||
| await writeJsonMcp(loc, apiKey, policy); | ||
| } else { | ||
| writeTomlMcp(loc, apiKey); | ||
| await writeTomlMcp(loc, apiKey, policy); | ||
| } | ||
| } | ||
| function writeJsonMcp(loc, apiKey) { | ||
| if (fs2.existsSync(loc.absolutePath)) { | ||
| try { | ||
| const existing = JSON.parse(fs2.readFileSync(loc.absolutePath, "utf-8")); | ||
| if (existing.mcpServers?.["01software"]) { | ||
| console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(01software already configured)")); | ||
| return; | ||
| } | ||
| existing.mcpServers = existing.mcpServers || {}; | ||
| existing.mcpServers["01software"] = getMcpServerEntry(apiKey); | ||
| fs2.writeFileSync(loc.absolutePath, JSON.stringify(existing, null, 2) + "\n"); | ||
| console.log(pc2.green(" Updated"), loc.displayPath); | ||
| } catch { | ||
| console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(could not parse existing file)")); | ||
| } | ||
| } else { | ||
| async function writeJsonMcp(loc, apiKey, policy) { | ||
| if (!fs2.existsSync(loc.absolutePath)) { | ||
| fs2.writeFileSync(loc.absolutePath, getMcpConfigTemplate(apiKey)); | ||
| console.log(pc2.green(" Created"), loc.displayPath); | ||
| return; | ||
| } | ||
| let existing; | ||
| try { | ||
| existing = JSON.parse(fs2.readFileSync(loc.absolutePath, "utf-8")); | ||
| } catch { | ||
| console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(could not parse existing file)")); | ||
| return; | ||
| } | ||
| const existingApiKey = existing.mcpServers?.["01software"]?.headers?.["x-api-key"]; | ||
| if (existingApiKey === apiKey) { | ||
| console.log(pc2.dim(" Unchanged"), loc.displayPath); | ||
| return; | ||
| } | ||
| if (apiKey === API_KEY_PLACEHOLDER && existingApiKey && existingApiKey !== API_KEY_PLACEHOLDER) { | ||
| console.log(pc2.dim(" Kept existing API key in"), loc.displayPath); | ||
| return; | ||
| } | ||
| if (existingApiKey) { | ||
| const shouldUpdate = await confirmKeyUpdate(loc.displayPath, policy); | ||
| if (!shouldUpdate) { | ||
| console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(kept existing API key)")); | ||
| return; | ||
| } | ||
| } | ||
| existing.mcpServers = existing.mcpServers || {}; | ||
| existing.mcpServers["01software"] = getMcpServerEntry(apiKey); | ||
| fs2.writeFileSync(loc.absolutePath, JSON.stringify(existing, null, 2) + "\n"); | ||
| console.log(pc2.green(" Updated"), loc.displayPath); | ||
| } | ||
| function writeTomlMcp(loc, apiKey) { | ||
| async function writeTomlMcp(loc, apiKey, policy) { | ||
| const section = getCodexMcpTomlSection(apiKey); | ||
| if (fs2.existsSync(loc.absolutePath)) { | ||
| const existing = fs2.readFileSync(loc.absolutePath, "utf-8"); | ||
| if (existing.includes(CODEX_MCP_SECTION_MARKER)) { | ||
| console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(01software already configured)")); | ||
| return; | ||
| } | ||
| if (!fs2.existsSync(loc.absolutePath)) { | ||
| fs2.writeFileSync(loc.absolutePath, section.trimStart()); | ||
| console.log(pc2.green(" Created"), loc.displayPath); | ||
| return; | ||
| } | ||
| const existing = fs2.readFileSync(loc.absolutePath, "utf-8"); | ||
| if (!existing.includes(CODEX_MCP_SECTION_MARKER)) { | ||
| const sep = existing.endsWith("\n") ? "" : "\n"; | ||
| fs2.appendFileSync(loc.absolutePath, sep + section); | ||
| console.log(pc2.green(" Updated"), loc.displayPath); | ||
| } else { | ||
| fs2.writeFileSync(loc.absolutePath, section.trimStart()); | ||
| console.log(pc2.green(" Created"), loc.displayPath); | ||
| return; | ||
| } | ||
| const existingApiKey = extractTomlApiKey(existing); | ||
| if (existingApiKey === apiKey) { | ||
| console.log(pc2.dim(" Unchanged"), loc.displayPath); | ||
| return; | ||
| } | ||
| if (apiKey === API_KEY_PLACEHOLDER && existingApiKey && existingApiKey !== API_KEY_PLACEHOLDER) { | ||
| console.log(pc2.dim(" Kept existing API key in"), loc.displayPath); | ||
| return; | ||
| } | ||
| const shouldUpdate = await confirmKeyUpdate(loc.displayPath, policy); | ||
| if (!shouldUpdate) { | ||
| console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(kept existing API key)")); | ||
| return; | ||
| } | ||
| const replaced = replaceTomlMcpSection(existing, section); | ||
| fs2.writeFileSync(loc.absolutePath, replaced); | ||
| console.log(pc2.green(" Updated"), loc.displayPath); | ||
| } | ||
| async function confirmKeyUpdate(displayPath, policy) { | ||
| if (policy === "overwrite") return true; | ||
| const { confirm } = await prompts2({ | ||
| type: "confirm", | ||
| name: "confirm", | ||
| message: `${displayPath} has a different 01software API key. Update?`, | ||
| initial: true | ||
| }); | ||
| return !!confirm; | ||
| } | ||
| function addToGitignore(cwd, tools) { | ||
@@ -707,3 +915,3 @@ const entries = []; | ||
| } | ||
| async function writeClaudeDocs(cwd, publishableKey, secretKey, tenantName) { | ||
| async function writeClaudeDocs(cwd, publishableKey, secretKey, tenantName, policy) { | ||
| let ctx = { | ||
@@ -745,3 +953,3 @@ tenantName: tenantName || "Your Tenant", | ||
| } else { | ||
| console.log(pc2.yellow(" Skipped"), ".claude/CLAUDE.md", pc2.dim("(@import already present)")); | ||
| console.log(pc2.dim(" Unchanged"), ".claude/CLAUDE.md"); | ||
| } | ||
@@ -752,14 +960,6 @@ } | ||
| const skillPath = path2.join(skillDir, "SKILL.md"); | ||
| if (!fs2.existsSync(skillPath)) { | ||
| fs2.mkdirSync(skillDir, { recursive: true }); | ||
| fs2.writeFileSync(skillPath, content); | ||
| console.log(pc2.green(" Created"), `.claude/skills/${dirName}/SKILL.md`); | ||
| } else { | ||
| console.log(pc2.yellow(" Skipped"), `.claude/skills/${dirName}/SKILL.md`, pc2.dim("(already exists)")); | ||
| } | ||
| fs2.mkdirSync(skillDir, { recursive: true }); | ||
| await writeFileWithPolicy(cwd, skillPath, content, policy); | ||
| } | ||
| } | ||
| function relativePath(cwd, filePath) { | ||
| return path2.relative(cwd, filePath); | ||
| } | ||
| var WS_FILE = "pnpm-workspace.yaml"; | ||
@@ -906,3 +1106,7 @@ var WS_BACKUP = "pnpm-workspace.yaml.bak"; | ||
| if (answers.aiTools.length > 0 && (!answers.publishableKey || !answers.secretKey)) { | ||
| console.log(pc3.dim(" Update MCP config x-api-key with your sk01_... token")); | ||
| console.log( | ||
| pc3.dim( | ||
| " Update MCP config x-api-key with your sk01_... or pat01_... bearer token" | ||
| ) | ||
| ); | ||
| console.log(); | ||
@@ -909,0 +1113,0 @@ } |
+1
-1
| { | ||
| "name": "@01.software/init", | ||
| "version": "0.5.1", | ||
| "version": "0.6.1", | ||
| "description": "Initialize 01.software SDK in your project (Next.js, React, Vanilla JS, Node.js, Edge)", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
143846
23.3%15
36.36%1448
23.44%