@01.software/init 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,9 +3,15 @@ import {
3
3
  fetchTenantContext,
4
4
  generateClaudeMd,
5
5
  getSkillFiles
6
- } from "./chunk-SRLZ5OIV.js";
6
+ } from "./chunk-T3A5SLEJ.js";
7
+ import {
8
+ readEnvValue,
9
+ replaceTomlMcpSection,
10
+ setEnvValue
11
+ } from "./chunk-JT3G6B66.js";
7
12
  import {
8
13
  CODEX_MCP_SECTION_MARKER,
14
+ getAnalyticsTemplate,
9
15
  getClientTemplate,
10
16
  getCodexMcpTomlSection,
11
17
  getEnvContent,
@@ -13,7 +19,7 @@ import {
13
19
  getMcpServerEntry,
14
20
  getQueryProviderTemplate,
15
21
  getServerTemplate
16
- } from "./chunk-OEAQV63E.js";
22
+ } from "./chunk-S3KHPWCE.js";
17
23
 
18
24
  // src/index.ts
19
25
  import pc3 from "picocolors";
@@ -26,19 +32,29 @@ function detectProject(cwd) {
26
32
  const hasPackageJson = fs.existsSync(pkgPath);
27
33
  let env = "node";
28
34
  let hasSdk = false;
35
+ let hasReactQuery = false;
29
36
  let parseError = false;
30
37
  if (hasPackageJson) {
31
38
  let pkg;
32
39
  try {
33
40
  pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
34
41
  } catch {
35
- return { hasPackageJson: true, parseError: true, env: "node", packageManager: null, hasSdk: false, srcDir: false };
42
+ return {
43
+ hasPackageJson: true,
44
+ parseError: true,
45
+ env: "node",
46
+ packageManager: null,
47
+ hasSdk: false,
48
+ hasReactQuery: false,
49
+ srcDir: false
50
+ };
36
51
  }
37
52
  const deps = {
38
53
  ...pkg.dependencies || {},
39
54
  ...pkg.devDependencies || {}
40
55
  };
41
56
  hasSdk = "@01.software/sdk" in deps;
57
+ hasReactQuery = "@tanstack/react-query" in deps;
42
58
  if ("next" in deps) {
43
59
  env = "nextjs";
44
60
  } else if ("astro" in deps || "@astrojs/node" in deps) {
@@ -68,7 +84,7 @@ function detectProject(cwd) {
68
84
  packageManager = "npm";
69
85
  }
70
86
  const srcDir = env === "nextjs" ? fs.existsSync(path.join(cwd, "src", "app")) : fs.existsSync(path.join(cwd, "src"));
71
- return { hasPackageJson, parseError, env, packageManager, hasSdk, srcDir };
87
+ return { hasPackageJson, parseError, env, packageManager, hasSdk, hasReactQuery, srcDir };
72
88
  }
73
89
  function needsClient(env) {
74
90
  return env === "nextjs" || env === "react-vite" || env === "react-cra" || env === "vanilla";
@@ -224,7 +240,7 @@ async function promptUser(hasSdk, detectedEnv, detectedPm) {
224
240
  {
225
241
  type: "select",
226
242
  name: "method",
227
- message: "API keys:",
243
+ message: "SDK credentials:",
228
244
  choices: [
229
245
  { title: "Browser login (recommended)", value: "browser" },
230
246
  { title: "Enter manually", value: "manual" },
@@ -259,6 +275,7 @@ import path2 from "path";
259
275
  import os from "os";
260
276
  import { execSync } from "child_process";
261
277
  import pc2 from "picocolors";
278
+ import prompts2 from "prompts";
262
279
 
263
280
  // src/browser-auth.ts
264
281
  import { randomBytes } from "crypto";
@@ -447,56 +464,57 @@ async function init(cwd, info, answers) {
447
464
  const wantsClient = needsClient(env);
448
465
  const wantsServer = needsServer(env);
449
466
  const wantsReactQuery = needsReactQuery(env);
450
- const deps = ["@01.software/sdk"];
451
- if (wantsReactQuery) deps.push("@tanstack/react-query");
452
- const addCmd = buildAddCmd(packageManager, hasPnpmWorkspace(cwd), deps);
453
- let installFailed = false;
454
- console.log(pc2.dim(` Installing ${deps.join(" and ")}...`));
455
- const wsPatched = packageManager === "pnpm" && patchPnpmWorkspace(cwd);
456
- try {
457
- execSync(addCmd, { cwd, stdio: "pipe" });
458
- console.log(pc2.green(" Installed"), deps.join(", "));
459
- } catch (error) {
460
- installFailed = true;
461
- const err = error;
462
- const msg = String(err.stderr || "").trim() || String(err.stdout || "").trim() || String(error);
463
- console.log(pc2.yellow(" Install failed \u2014 continuing with scaffolding"));
464
- const firstLines = msg.split("\n").slice(0, 3).map((l) => ` ${l}`).join("\n");
465
- if (firstLines) console.log(pc2.dim(firstLines));
466
- console.log(pc2.dim(` Run manually: ${addCmd}`));
467
- } finally {
468
- if (wsPatched) restorePnpmWorkspace(cwd);
467
+ const plan = await planConflictsAndEnv(cwd, baseDir, env, answers);
468
+ const installResult = installDeps(
469
+ cwd,
470
+ packageManager,
471
+ info.hasSdk,
472
+ info.hasReactQuery,
473
+ wantsReactQuery
474
+ );
475
+ if (wantsClient || wantsReactQuery || wantsServer) {
476
+ fs2.mkdirSync(path2.join(baseDir, "lib", "software"), { recursive: true });
469
477
  }
470
478
  const libDir = path2.join(baseDir, "lib", "software");
471
- fs2.mkdirSync(libDir, { recursive: true });
472
479
  if (wantsClient) {
473
- writeFileIfAbsent(
480
+ await writeFileWithPolicy(
474
481
  cwd,
475
482
  path2.join(libDir, "client.ts"),
476
- getClientTemplate(env, publishableKeyEnvVar)
483
+ getClientTemplate(env, publishableKeyEnvVar),
484
+ plan.policy
485
+ );
486
+ await writeFileWithPolicy(
487
+ cwd,
488
+ path2.join(libDir, "analytics.ts"),
489
+ getAnalyticsTemplate(env, publishableKeyEnvVar),
490
+ plan.policy
477
491
  );
478
492
  }
479
493
  if (wantsReactQuery) {
480
- writeFileIfAbsent(
494
+ await writeFileWithPolicy(
481
495
  cwd,
482
496
  path2.join(libDir, "query-provider.tsx"),
483
- getQueryProviderTemplate(env)
497
+ getQueryProviderTemplate(env),
498
+ plan.policy
484
499
  );
485
500
  }
486
501
  if (wantsServer) {
487
- writeFileIfAbsent(
502
+ await writeFileWithPolicy(
488
503
  cwd,
489
504
  path2.join(libDir, "server.ts"),
490
- getServerTemplate(env, publishableKeyEnvVar, SECRET_KEY_ENV_VAR)
505
+ getServerTemplate(env, publishableKeyEnvVar, SECRET_KEY_ENV_VAR),
506
+ plan.policy
491
507
  );
492
508
  }
493
- if (env !== "vanilla" && env !== "edge" && answers.authMethod !== "browser") {
494
- writeEnv(
509
+ if (plan.envFile && answers.authMethod !== "browser") {
510
+ await writeEnv(
495
511
  cwd,
512
+ plan.envFile,
496
513
  answers.publishableKey || "",
497
514
  answers.secretKey || "",
498
515
  publishableKeyEnvVar,
499
- wantsServer ? SECRET_KEY_ENV_VAR : null
516
+ wantsServer ? SECRET_KEY_ENV_VAR : null,
517
+ plan.policy
500
518
  );
501
519
  }
502
520
  let publishableKey = answers.publishableKey;
@@ -509,13 +527,15 @@ async function init(cwd, info, answers) {
509
527
  publishableKey = creds.publishableKey;
510
528
  secretKey = creds.secretKey;
511
529
  tenantName = creds.tenantName;
512
- if (env !== "vanilla" && env !== "edge" && publishableKey) {
513
- writeEnv(
530
+ if (plan.envFile && publishableKey) {
531
+ await writeEnv(
514
532
  cwd,
533
+ plan.envFile,
515
534
  publishableKey,
516
535
  secretKey,
517
536
  publishableKeyEnvVar,
518
537
  wantsServer ? SECRET_KEY_ENV_VAR : null,
538
+ "overwrite",
519
539
  true
520
540
  );
521
541
  }
@@ -527,16 +547,47 @@ async function init(cwd, info, answers) {
527
547
  }
528
548
  }
529
549
  if (answers.aiTools.length > 0) {
530
- const apiKey = secretKey || "YOUR_API_KEY";
531
550
  for (const tool of answers.aiTools) {
532
- writeMcpConfig(tool, cwd, apiKey);
551
+ await writeMcpConfig(tool, cwd);
533
552
  }
534
553
  addToGitignore(cwd, answers.aiTools);
535
554
  if (answers.aiTools.includes("claude")) {
536
- await writeClaudeDocs(cwd, publishableKey, secretKey, tenantName);
555
+ await writeClaudeDocs(cwd, publishableKey, secretKey, tenantName, plan.policy);
537
556
  }
538
557
  }
539
- return { installFailed, installCmd: addCmd };
558
+ return installResult;
559
+ }
560
+ function installDeps(cwd, pm, hasSdk, hasReactQuery, wantsReactQuery) {
561
+ const allDeps = [
562
+ { name: "@01.software/sdk", installed: hasSdk, needed: true },
563
+ { name: "@tanstack/react-query", installed: hasReactQuery, needed: wantsReactQuery }
564
+ ];
565
+ const fullList = allDeps.filter((d) => d.needed).map((d) => d.name);
566
+ const toInstall = allDeps.filter((d) => d.needed && !d.installed).map((d) => d.name);
567
+ const fullCmd = buildAddCmd(pm, hasPnpmWorkspace(cwd), fullList);
568
+ if (toInstall.length === 0) {
569
+ console.log(pc2.dim(` Dependencies already installed: ${fullList.join(", ")}`));
570
+ return { installFailed: false, installSkipped: true, installCmd: fullCmd };
571
+ }
572
+ const addCmd = buildAddCmd(pm, hasPnpmWorkspace(cwd), toInstall);
573
+ console.log(pc2.dim(` Installing ${toInstall.join(" and ")}...`));
574
+ const wsPatched = pm === "pnpm" && patchPnpmWorkspace(cwd);
575
+ let installFailed = false;
576
+ try {
577
+ execSync(addCmd, { cwd, stdio: "pipe" });
578
+ console.log(pc2.green(" Installed"), toInstall.join(", "));
579
+ } catch (error) {
580
+ installFailed = true;
581
+ const err = error;
582
+ const msg = String(err.stderr || "").trim() || String(err.stdout || "").trim() || String(error);
583
+ console.log(pc2.yellow(" Install failed \u2014 continuing with scaffolding"));
584
+ const firstLines = msg.split("\n").slice(0, 3).map((l) => ` ${l}`).join("\n");
585
+ if (firstLines) console.log(pc2.dim(firstLines));
586
+ console.log(pc2.dim(` Run manually: ${addCmd}`));
587
+ } finally {
588
+ if (wsPatched) restorePnpmWorkspace(cwd);
589
+ }
590
+ return { installFailed, installSkipped: false, installCmd: addCmd };
540
591
  }
541
592
  function buildAddCmd(pm, hasPnpmWs, deps) {
542
593
  const pkgs = deps.join(" ");
@@ -551,34 +602,155 @@ function buildAddCmd(pm, hasPnpmWs, deps) {
551
602
  return `npm install ${pkgs}`;
552
603
  }
553
604
  }
554
- function writeFileIfAbsent(cwd, filePath, content) {
555
- if (fs2.existsSync(filePath)) {
556
- console.log(pc2.yellow(" Skipped"), relativePath(cwd, filePath), pc2.dim("(already exists)"));
605
+ async function planConflictsAndEnv(cwd, baseDir, env, answers) {
606
+ const candidates = [];
607
+ const libDir = path2.join(baseDir, "lib", "software");
608
+ if (needsClient(env)) candidates.push(path2.join(libDir, "client.ts"));
609
+ if (needsReactQuery(env)) candidates.push(path2.join(libDir, "query-provider.tsx"));
610
+ if (needsServer(env)) candidates.push(path2.join(libDir, "server.ts"));
611
+ if (answers.aiTools.includes("claude")) {
612
+ for (const { dirName } of getSkillFiles()) {
613
+ candidates.push(path2.join(cwd, ".claude", "skills", dirName, "SKILL.md"));
614
+ }
615
+ }
616
+ const conflicts = candidates.filter((p) => fs2.existsSync(p));
617
+ let policy = "skip";
618
+ if (conflicts.length > 0) {
619
+ console.log(pc2.yellow(` ${conflicts.length} file(s) already exist:`));
620
+ for (const c of conflicts) console.log(pc2.dim(` ${path2.relative(cwd, c)}`));
621
+ const { selected } = await prompts2({
622
+ type: "select",
623
+ name: "selected",
624
+ message: "How should I handle existing files?",
625
+ choices: [
626
+ { title: "Keep existing (skip)", value: "skip" },
627
+ { title: "Overwrite all", value: "overwrite" },
628
+ { title: "Ask for each", value: "ask" }
629
+ ],
630
+ initial: 0
631
+ });
632
+ policy = selected ?? "skip";
633
+ }
634
+ const envFile = env === "vanilla" || env === "edge" ? "" : await pickEnvFile(cwd, env);
635
+ return { policy, envFile };
636
+ }
637
+ async function pickEnvFile(cwd, env) {
638
+ const candidates = [".env.local", ".env", ".env.development"];
639
+ const existing = candidates.filter((f) => fs2.existsSync(path2.join(cwd, f)));
640
+ const preferred = env === "nextjs" ? ".env.local" : ".env";
641
+ if (existing.length === 0) return preferred;
642
+ if (existing.length === 1 && existing[0] === preferred) return existing[0];
643
+ const options = Array.from(/* @__PURE__ */ new Set([...existing, preferred]));
644
+ const choices = options.map((f) => ({
645
+ title: f,
646
+ description: existing.includes(f) ? "exists" : "create",
647
+ value: f
648
+ }));
649
+ const initial = Math.max(
650
+ 0,
651
+ choices.findIndex((c) => c.value === preferred)
652
+ );
653
+ const { file } = await prompts2({
654
+ type: "select",
655
+ name: "file",
656
+ message: "Which env file should I write SDK credentials to?",
657
+ choices,
658
+ initial
659
+ });
660
+ return file ?? preferred;
661
+ }
662
+ async function writeFileWithPolicy(cwd, filePath, content, policy) {
663
+ const rel = path2.relative(cwd, filePath);
664
+ if (!fs2.existsSync(filePath)) {
665
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
666
+ fs2.writeFileSync(filePath, content);
667
+ console.log(pc2.green(" Created"), rel);
668
+ return;
669
+ }
670
+ const existing = fs2.readFileSync(filePath, "utf-8");
671
+ if (existing === content) {
672
+ console.log(pc2.dim(" Unchanged"), rel);
557
673
  return;
558
674
  }
559
- fs2.writeFileSync(filePath, content);
560
- console.log(pc2.green(" Created"), relativePath(cwd, filePath));
675
+ let shouldWrite = false;
676
+ if (policy === "overwrite") {
677
+ shouldWrite = true;
678
+ } else if (policy === "ask") {
679
+ const { confirm } = await prompts2({
680
+ type: "confirm",
681
+ name: "confirm",
682
+ message: `Overwrite ${rel}?`,
683
+ initial: false
684
+ });
685
+ shouldWrite = !!confirm;
686
+ }
687
+ if (shouldWrite) {
688
+ fs2.writeFileSync(filePath, content);
689
+ console.log(pc2.green(" Overwrote"), rel);
690
+ } else {
691
+ console.log(pc2.yellow(" Skipped"), rel, pc2.dim("(already exists)"));
692
+ }
561
693
  }
562
- function writeEnv(cwd, publishableKey, secretKey, publishableKeyEnvVar, secretKeyEnvVar, afterBrowserAuth = false) {
563
- const envPath = path2.join(cwd, ".env");
564
- const envContent = getEnvContent(publishableKey, secretKey, publishableKeyEnvVar, secretKeyEnvVar);
565
- if (fs2.existsSync(envPath)) {
566
- const existing = fs2.readFileSync(envPath, "utf-8");
567
- if (existing.includes(publishableKeyEnvVar)) {
568
- if (!afterBrowserAuth) {
569
- console.log(pc2.yellow(" Skipped"), ".env", pc2.dim("(keys already present)"));
694
+ async function writeEnv(cwd, envFile, publishableKey, secretKey, publishableKeyEnvVar, secretKeyEnvVar, policy, fromBrowserAuth = false) {
695
+ const envPath = path2.join(cwd, envFile);
696
+ const targets = [
697
+ { name: publishableKeyEnvVar, value: publishableKey }
698
+ ];
699
+ if (secretKeyEnvVar) {
700
+ targets.push({ name: secretKeyEnvVar, value: secretKey });
701
+ }
702
+ if (!fs2.existsSync(envPath)) {
703
+ const initial = getEnvContent(publishableKey, secretKey, publishableKeyEnvVar, secretKeyEnvVar);
704
+ fs2.writeFileSync(envPath, initial.trimStart());
705
+ console.log(pc2.green(" Created"), envFile);
706
+ return;
707
+ }
708
+ let content = fs2.readFileSync(envPath, "utf-8");
709
+ let modified = false;
710
+ let appendedHeader = false;
711
+ const headerAlreadyPresent = targets.some(
712
+ (t) => readEnvValue(content, t.name) !== null
713
+ );
714
+ for (const { name, value } of targets) {
715
+ const existing = readEnvValue(content, name);
716
+ if (existing === null) {
717
+ if (!headerAlreadyPresent && !appendedHeader) {
718
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
719
+ content += "\n# 01.software\n";
720
+ appendedHeader = true;
570
721
  }
571
- return;
722
+ content = setEnvValue(content, name, value);
723
+ modified = true;
724
+ continue;
725
+ }
726
+ if (existing === value) continue;
727
+ if (!value) continue;
728
+ let shouldOverwrite = false;
729
+ if (policy === "overwrite") {
730
+ shouldOverwrite = true;
731
+ } else if (policy === "ask") {
732
+ const { confirm } = await prompts2({
733
+ type: "confirm",
734
+ name: "confirm",
735
+ message: `${name} already set in ${envFile}. Overwrite?`,
736
+ initial: fromBrowserAuth
737
+ });
738
+ shouldOverwrite = !!confirm;
739
+ }
740
+ if (shouldOverwrite) {
741
+ content = setEnvValue(content, name, value);
742
+ modified = true;
572
743
  }
573
- fs2.appendFileSync(envPath, envContent);
744
+ }
745
+ if (modified) {
746
+ fs2.writeFileSync(envPath, content);
574
747
  console.log(
575
748
  pc2.green(" Updated"),
576
- ".env",
577
- afterBrowserAuth ? pc2.dim("(added API keys)") : ""
749
+ envFile,
750
+ fromBrowserAuth ? pc2.dim("(SDK credentials)") : ""
578
751
  );
579
752
  } else {
580
- fs2.writeFileSync(envPath, envContent.trimStart());
581
- console.log(pc2.green(" Created"), ".env");
753
+ console.log(pc2.dim(" Unchanged"), envFile);
582
754
  }
583
755
  }
584
756
  function resolveMcpLocation(tool, cwd) {
@@ -608,7 +780,13 @@ function resolveMcpLocation(tool, cwd) {
608
780
  case "windsurf": {
609
781
  if (!home) return null;
610
782
  const p = path2.join(home, ".codeium", "windsurf", "mcp_config.json");
611
- return { kind: "json", absolutePath: p, displayPath: p, gitignoreEntry: null };
783
+ return {
784
+ kind: "json",
785
+ absolutePath: p,
786
+ jsonClient: "windsurf",
787
+ displayPath: p,
788
+ gitignoreEntry: null
789
+ };
612
790
  }
613
791
  case "codex": {
614
792
  if (!home) return null;
@@ -622,7 +800,7 @@ function resolveMcpLocation(tool, cwd) {
622
800
  }
623
801
  }
624
802
  }
625
- function writeMcpConfig(tool, cwd, apiKey) {
803
+ async function writeMcpConfig(tool, cwd) {
626
804
  const loc = resolveMcpLocation(tool, cwd);
627
805
  if (!loc) {
628
806
  console.log(pc2.yellow(` Skipped ${tool}`), pc2.dim("(HOME not set)"));
@@ -630,46 +808,55 @@ function writeMcpConfig(tool, cwd, apiKey) {
630
808
  }
631
809
  fs2.mkdirSync(path2.dirname(loc.absolutePath), { recursive: true });
632
810
  if (loc.kind === "json") {
633
- writeJsonMcp(loc, apiKey);
811
+ writeJsonMcp(loc);
634
812
  } else {
635
- writeTomlMcp(loc, apiKey);
813
+ writeTomlMcp(loc);
636
814
  }
637
815
  }
638
- function writeJsonMcp(loc, apiKey) {
639
- if (fs2.existsSync(loc.absolutePath)) {
640
- try {
641
- const existing = JSON.parse(fs2.readFileSync(loc.absolutePath, "utf-8"));
642
- if (existing.mcpServers?.["01software"]) {
643
- console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(01software already configured)"));
644
- return;
645
- }
646
- existing.mcpServers = existing.mcpServers || {};
647
- existing.mcpServers["01software"] = getMcpServerEntry(apiKey);
648
- fs2.writeFileSync(loc.absolutePath, JSON.stringify(existing, null, 2) + "\n");
649
- console.log(pc2.green(" Updated"), loc.displayPath);
650
- } catch {
651
- console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(could not parse existing file)"));
652
- }
653
- } else {
654
- fs2.writeFileSync(loc.absolutePath, getMcpConfigTemplate(apiKey));
816
+ function writeJsonMcp(loc) {
817
+ if (!fs2.existsSync(loc.absolutePath)) {
818
+ fs2.writeFileSync(loc.absolutePath, getMcpConfigTemplate(loc.jsonClient));
655
819
  console.log(pc2.green(" Created"), loc.displayPath);
820
+ return;
656
821
  }
822
+ let existing;
823
+ try {
824
+ existing = JSON.parse(fs2.readFileSync(loc.absolutePath, "utf-8"));
825
+ } catch {
826
+ console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(could not parse existing file)"));
827
+ return;
828
+ }
829
+ const nextEntry = getMcpServerEntry(loc.jsonClient);
830
+ if (existing.mcpServers?.["01software"] && JSON.stringify(existing.mcpServers["01software"]) === JSON.stringify(nextEntry)) {
831
+ console.log(pc2.dim(" Unchanged"), loc.displayPath);
832
+ return;
833
+ }
834
+ existing.mcpServers = existing.mcpServers || {};
835
+ existing.mcpServers["01software"] = nextEntry;
836
+ fs2.writeFileSync(loc.absolutePath, JSON.stringify(existing, null, 2) + "\n");
837
+ console.log(pc2.green(" Updated"), loc.displayPath);
657
838
  }
658
- function writeTomlMcp(loc, apiKey) {
659
- const section = getCodexMcpTomlSection(apiKey);
660
- if (fs2.existsSync(loc.absolutePath)) {
661
- const existing = fs2.readFileSync(loc.absolutePath, "utf-8");
662
- if (existing.includes(CODEX_MCP_SECTION_MARKER)) {
663
- console.log(pc2.yellow(" Skipped"), loc.displayPath, pc2.dim("(01software already configured)"));
664
- return;
665
- }
839
+ function writeTomlMcp(loc) {
840
+ const section = getCodexMcpTomlSection();
841
+ if (!fs2.existsSync(loc.absolutePath)) {
842
+ fs2.writeFileSync(loc.absolutePath, section.trimStart());
843
+ console.log(pc2.green(" Created"), loc.displayPath);
844
+ return;
845
+ }
846
+ const existing = fs2.readFileSync(loc.absolutePath, "utf-8");
847
+ if (!existing.includes(CODEX_MCP_SECTION_MARKER)) {
666
848
  const sep = existing.endsWith("\n") ? "" : "\n";
667
849
  fs2.appendFileSync(loc.absolutePath, sep + section);
668
850
  console.log(pc2.green(" Updated"), loc.displayPath);
669
- } else {
670
- fs2.writeFileSync(loc.absolutePath, section.trimStart());
671
- console.log(pc2.green(" Created"), loc.displayPath);
851
+ return;
852
+ }
853
+ const replaced = replaceTomlMcpSection(existing, section);
854
+ if (replaced === existing) {
855
+ console.log(pc2.dim(" Unchanged"), loc.displayPath);
856
+ return;
672
857
  }
858
+ fs2.writeFileSync(loc.absolutePath, replaced);
859
+ console.log(pc2.green(" Updated"), loc.displayPath);
673
860
  }
674
861
  function addToGitignore(cwd, tools) {
675
862
  const entries = [];
@@ -682,7 +869,7 @@ function addToGitignore(cwd, tools) {
682
869
  const existing = fs2.existsSync(gitignorePath) ? fs2.readFileSync(gitignorePath, "utf-8") : "";
683
870
  const toAdd = entries.filter((e) => !existing.includes(e));
684
871
  if (toAdd.length === 0) return;
685
- const content = "\n# MCP configs (contain API key)\n" + toAdd.join("\n") + "\n";
872
+ const content = "\n# MCP configs\n" + toAdd.join("\n") + "\n";
686
873
  if (fs2.existsSync(gitignorePath)) {
687
874
  fs2.appendFileSync(gitignorePath, content);
688
875
  } else {
@@ -690,7 +877,7 @@ function addToGitignore(cwd, tools) {
690
877
  }
691
878
  console.log(pc2.green(" Updated"), ".gitignore", pc2.dim(`(added ${toAdd.join(", ")})`));
692
879
  }
693
- async function writeClaudeDocs(cwd, publishableKey, secretKey, tenantName) {
880
+ async function writeClaudeDocs(cwd, publishableKey, secretKey, tenantName, policy) {
694
881
  let ctx = {
695
882
  tenantName: tenantName || "Your Tenant",
696
883
  features: void 0,
@@ -727,24 +914,16 @@ async function writeClaudeDocs(cwd, publishableKey, secretKey, tenantName) {
727
914
  fs2.appendFileSync(claudeMdPath, prefix + importLine + "\n");
728
915
  console.log(pc2.green(" Updated"), ".claude/CLAUDE.md", pc2.dim("(added @import)"));
729
916
  } else {
730
- console.log(pc2.yellow(" Skipped"), ".claude/CLAUDE.md", pc2.dim("(@import already present)"));
917
+ console.log(pc2.dim(" Unchanged"), ".claude/CLAUDE.md");
731
918
  }
732
919
  }
733
920
  for (const { dirName, content } of getSkillFiles()) {
734
921
  const skillDir = path2.join(skillsDir, dirName);
735
922
  const skillPath = path2.join(skillDir, "SKILL.md");
736
- if (!fs2.existsSync(skillPath)) {
737
- fs2.mkdirSync(skillDir, { recursive: true });
738
- fs2.writeFileSync(skillPath, content);
739
- console.log(pc2.green(" Created"), `.claude/skills/${dirName}/SKILL.md`);
740
- } else {
741
- console.log(pc2.yellow(" Skipped"), `.claude/skills/${dirName}/SKILL.md`, pc2.dim("(already exists)"));
742
- }
923
+ fs2.mkdirSync(skillDir, { recursive: true });
924
+ await writeFileWithPolicy(cwd, skillPath, content, policy);
743
925
  }
744
926
  }
745
- function relativePath(cwd, filePath) {
746
- return path2.relative(cwd, filePath);
747
- }
748
927
  var WS_FILE = "pnpm-workspace.yaml";
749
928
  var WS_BACKUP = "pnpm-workspace.yaml.bak";
750
929
  function hasPnpmWorkspace(cwd) {
@@ -854,23 +1033,35 @@ async function main() {
854
1033
  console.log(pc3.cyan(" import { QueryProvider } from '@/lib/software/query-provider'"));
855
1034
  console.log(pc3.cyan(" <QueryProvider>{children}</QueryProvider>"));
856
1035
  console.log();
1036
+ console.log(pc3.dim(" Optional: start browser analytics with the generated helper:"));
1037
+ console.log();
1038
+ console.log(pc3.cyan(" import { analytics } from '@/lib/software/analytics'"));
1039
+ console.log(pc3.cyan(" analytics.track('signup')"));
1040
+ console.log();
857
1041
  } else if (env === "react-vite" || env === "react-cra") {
858
1042
  console.log(pc3.dim(" Wrap your app entry with QueryProvider:"));
859
1043
  console.log();
860
1044
  console.log(pc3.cyan(" import { QueryProvider } from './lib/software/query-provider'"));
861
1045
  console.log(pc3.cyan(" <QueryProvider><App /></QueryProvider>"));
862
1046
  console.log();
1047
+ console.log(pc3.dim(" Optional: start browser analytics with the generated helper:"));
1048
+ console.log();
1049
+ console.log(pc3.cyan(" import { analytics } from './lib/software/analytics'"));
1050
+ console.log(pc3.cyan(" analytics.track('signup')"));
1051
+ console.log();
863
1052
  } else if (env === "vanilla") {
864
1053
  console.log(pc3.dim(" Replace YOUR_PUBLISHABLE_KEY in lib/software/client.ts"));
865
1054
  console.log();
866
1055
  console.log(pc3.cyan(" import { client } from './lib/software/client'"));
867
- console.log(pc3.cyan(" const posts = await client.from('posts').find()"));
1056
+ console.log(pc3.cyan(" const articles = await client.from('articles').find()"));
1057
+ console.log();
1058
+ console.log(pc3.dim(" Optional: wire the analytics helper in lib/software/analytics.ts"));
868
1059
  console.log();
869
1060
  } else if (env === "node") {
870
1061
  console.log(pc3.dim(" Use the server client:"));
871
1062
  console.log();
872
1063
  console.log(pc3.cyan(" import { serverClient } from './lib/software/server'"));
873
- console.log(pc3.cyan(" const posts = await serverClient.from('posts').find()"));
1064
+ console.log(pc3.cyan(" const articles = await serverClient.from('articles').find()"));
874
1065
  console.log();
875
1066
  } else if (env === "edge") {
876
1067
  console.log(pc3.dim(" Pass your env bindings to createEdgeClient():"));
@@ -882,11 +1073,11 @@ async function main() {
882
1073
  const missingPublishableKey = env !== "vanilla" && !answers.publishableKey;
883
1074
  const missingSecretKey = (env === "nextjs" || env === "node") && !answers.secretKey;
884
1075
  if (missingPublishableKey || missingSecretKey) {
885
- console.log(pc3.dim(" Update .env with your API keys"));
1076
+ console.log(pc3.dim(" Update .env with your SDK credentials"));
886
1077
  console.log();
887
1078
  }
888
- if (answers.aiTools.length > 0 && (!answers.publishableKey || !answers.secretKey)) {
889
- console.log(pc3.dim(" Update MCP config x-api-key with your sk01_... token"));
1079
+ if (answers.aiTools.length > 0) {
1080
+ console.log(pc3.dim(" MCP config uses OAuth discovery."));
890
1081
  console.log();
891
1082
  }
892
1083
  if (env !== "vanilla") {