joycraft 0.6.14 → 0.6.16

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.
@@ -7,7 +7,7 @@ import {
7
7
  PRIVATE_UNTRACK_COMMAND,
8
8
  applyGitignoreProfile,
9
9
  resolveGitignoreProfile
10
- } from "./chunk-VIVJUY6J.js";
10
+ } from "./chunk-G6WSFZQG.js";
11
11
  import {
12
12
  CODEX_SKILLS,
13
13
  PI_AGENTS,
@@ -16,18 +16,19 @@ import {
16
16
  PI_SKILLS,
17
17
  SKILLS,
18
18
  TEMPLATES
19
- } from "./chunk-VCLRPD62.js";
19
+ } from "./chunk-UEG5IO6Q.js";
20
20
  import {
21
21
  DEFAULT_GITIGNORE_PROFILE,
22
22
  STATE_PATH,
23
23
  hashContent,
24
24
  readVersion,
25
+ resolveHarnesses,
25
26
  writeVersion
26
- } from "./chunk-TD65VH2W.js";
27
+ } from "./chunk-34IWIKXS.js";
27
28
 
28
29
  // src/init.ts
29
- import { mkdirSync as mkdirSync2, existsSync as existsSync4, writeFileSync as writeFileSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync, chmodSync } from "fs";
30
- import { join as join4, basename, resolve, dirname } from "path";
30
+ import { mkdirSync as mkdirSync2, existsSync as existsSync5, writeFileSync as writeFileSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync, chmodSync } from "fs";
31
+ import { join as join5, basename, resolve, dirname } from "path";
31
32
 
32
33
  // src/detect.ts
33
34
  import { readFileSync, existsSync, readdirSync } from "fs";
@@ -310,6 +311,10 @@ async function detectStack(dir) {
310
311
  // src/improve-claude-md.ts
311
312
  import { existsSync as existsSync2 } from "fs";
312
313
  import { join as join2 } from "path";
314
+ var PRIVATE_SETUP_NOTE_MARKER = "After cloning, run";
315
+ function generatePrivateSetupNote() {
316
+ return `> **Private setup:** The harness dirs (\`.claude/\`, \`.agents/\`, \`.pi/\`) are gitignored in this repo, so they aren't committed. ${PRIVATE_SETUP_NOTE_MARKER} \`npx joycraft init\` to regenerate the skill files locally.`;
317
+ }
313
318
  function generateCommandsBlock(stack) {
314
319
  const lines = ["```bash"];
315
320
  if (stack.commands.build) lines.push(`# Build
@@ -453,6 +458,9 @@ function generateCLAUDEMd(projectName, stack, existingSkills = [], opts) {
453
458
  generateGettingStartedSection(),
454
459
  ""
455
460
  ];
461
+ if (opts?.privateProfile) {
462
+ lines.push(generatePrivateSetupNote(), "");
463
+ }
456
464
  if (existingSkills.length > 0) {
457
465
  lines.push(generateProjectToolsSection(existingSkills), "");
458
466
  }
@@ -493,7 +501,7 @@ function generateKeyFilesSection2() {
493
501
  |------|---------|
494
502
  | _TODO_ | _Add key files_ |`;
495
503
  }
496
- function generateAgentsMd(projectName, stack) {
504
+ function generateAgentsMd(projectName, stack, privateProfile = false) {
497
505
  const frameworkNote = stack.framework ? ` (${stack.framework})` : "";
498
506
  const langLabel = stack.language === "unknown" ? "" : ` | **Stack:** ${stack.language}${frameworkNote}`;
499
507
  const lines = [
@@ -515,6 +523,9 @@ function generateAgentsMd(projectName, stack) {
515
523
  generateDevelopmentSection(stack),
516
524
  ""
517
525
  ];
526
+ if (privateProfile) {
527
+ lines.push(generatePrivateSetupNote(), "");
528
+ }
518
529
  return lines.join("\n");
519
530
  }
520
531
 
@@ -702,39 +713,122 @@ function installSafeguardHooks(targetDir, customPatterns = [], force = false, sk
702
713
  return result;
703
714
  }
704
715
 
716
+ // src/tsconfig.ts
717
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
718
+ import { join as join4 } from "path";
719
+ var PI_EXCLUDE = ".pi";
720
+ function stripJsonComments(text) {
721
+ return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
722
+ }
723
+ function alreadyExcludesPi(rawText) {
724
+ try {
725
+ const parsed = JSON.parse(stripJsonComments(rawText));
726
+ if (!Array.isArray(parsed.exclude)) return false;
727
+ return parsed.exclude.some(
728
+ (e) => typeof e === "string" && (e === PI_EXCLUDE || e === "./.pi" || e === ".pi/**")
729
+ );
730
+ } catch {
731
+ return false;
732
+ }
733
+ }
734
+ function insertPiExclude(rawText) {
735
+ const excludeArrayRe = /("exclude"\s*:\s*\[)(\s*)/;
736
+ if (excludeArrayRe.test(rawText)) {
737
+ return rawText.replace(excludeArrayRe, (_m, open, ws) => {
738
+ const sep = ws.includes("\n") ? ws : " ";
739
+ return `${open}${sep}"${PI_EXCLUDE}",${ws.includes("\n") ? "" : " "}`;
740
+ });
741
+ }
742
+ const includeArrayRe = /("include"\s*:\s*\[[^\]]*\])/;
743
+ if (includeArrayRe.test(rawText)) {
744
+ return rawText.replace(includeArrayRe, (m) => `${m},
745
+ "exclude": ["${PI_EXCLUDE}"]`);
746
+ }
747
+ const lastBrace = rawText.lastIndexOf("}");
748
+ if (lastBrace > 0 && rawText.slice(0, lastBrace).includes(":")) {
749
+ const before = rawText.slice(0, lastBrace).replace(/\s*$/, "");
750
+ const after = rawText.slice(lastBrace);
751
+ const needsComma = !before.endsWith(",") && !before.endsWith("{");
752
+ return `${before}${needsComma ? "," : ""}
753
+ "exclude": ["${PI_EXCLUDE}"]
754
+ ${after}`;
755
+ }
756
+ return null;
757
+ }
758
+ function ensurePiExcludedFromTsconfig(targetDir) {
759
+ const path = join4(targetDir, "tsconfig.json");
760
+ if (!existsSync4(path)) return { status: "no-tsconfig" };
761
+ let rawText;
762
+ try {
763
+ rawText = readFileSync3(path, "utf-8");
764
+ } catch {
765
+ return { status: "skipped", reason: "tsconfig.json could not be read" };
766
+ }
767
+ if (alreadyExcludesPi(rawText)) {
768
+ return { status: "already-present", path };
769
+ }
770
+ const updated = insertPiExclude(rawText);
771
+ if (updated === null || updated === rawText) {
772
+ return {
773
+ status: "skipped",
774
+ reason: 'could not safely edit tsconfig.json \u2014 add ".pi" to its "exclude" array manually so the Pi extension stays out of your TypeScript build'
775
+ };
776
+ }
777
+ if (!alreadyExcludesPi(updated)) {
778
+ return {
779
+ status: "skipped",
780
+ reason: 'tsconfig.json edit could not be verified \u2014 add ".pi" to its "exclude" array manually'
781
+ };
782
+ }
783
+ try {
784
+ writeFileSync2(path, updated, "utf-8");
785
+ } catch {
786
+ return { status: "skipped", reason: "tsconfig.json could not be written" };
787
+ }
788
+ return { status: "added", path };
789
+ }
790
+
705
791
  // src/init.ts
706
792
  function ensureDir(dir) {
707
- if (!existsSync4(dir)) {
793
+ if (!existsSync5(dir)) {
708
794
  mkdirSync2(dir, { recursive: true });
709
795
  }
710
796
  }
711
797
  function writeFile(path, content, force, result) {
712
- if (existsSync4(path) && !force) {
798
+ if (existsSync5(path) && !force) {
713
799
  result.skipped.push(path);
714
800
  return;
715
801
  }
716
- writeFileSync2(path, content, "utf-8");
802
+ writeFileSync3(path, content, "utf-8");
717
803
  result.created.push(path);
718
804
  }
719
805
  async function init(dir, opts) {
720
806
  const targetDir = resolve(dir);
721
807
  const result = { created: [], skipped: [], modified: [], warnings: [] };
722
808
  const stack = await detectStack(targetDir);
723
- const isPi = existsSync4(join4(targetDir, ".pi"));
809
+ const isPi = existsSync5(join5(targetDir, ".pi"));
810
+ const harnesses = await resolveHarnesses(process.stdin.isTTY === true);
811
+ const wants = (h) => harnesses.includes(h);
812
+ if (harnesses.length === 0) {
813
+ console.log(
814
+ "\nNo harness selected \u2014 Joycraft will not install any skills.\nPlease run init again and select at least one harness (claude, codex, pi)."
815
+ );
816
+ return;
817
+ }
724
818
  const { profile: gitignoreProfile } = await resolveGitignoreProfile({
725
819
  flag: opts.gitignore,
726
820
  persisted: readVersion(targetDir)?.gitignoreProfile,
727
821
  interactive: process.stdin.isTTY === true,
728
822
  promptIntro: "\nHow should Joycraft files be tracked in git?"
729
823
  });
730
- ensureDir(join4(targetDir, "docs", "context"));
731
- const skillsDir = join4(targetDir, ".claude", "skills");
824
+ ensureDir(join5(targetDir, "docs", "context"));
825
+ const skillsDir = join5(targetDir, ".claude", "skills");
732
826
  let existingSkills = [];
733
- if (existsSync4(skillsDir)) {
827
+ if (wants("claude") && existsSync5(skillsDir)) {
734
828
  existingSkills = readdirSync2(skillsDir).filter((name) => {
735
829
  if (name.startsWith("joycraft-")) return false;
736
830
  if (name.startsWith(".")) return false;
737
- const fullPath = join4(skillsDir, name);
831
+ const fullPath = join5(skillsDir, name);
738
832
  try {
739
833
  return statSync(fullPath).isDirectory();
740
834
  } catch {
@@ -742,115 +836,128 @@ async function init(dir, opts) {
742
836
  }
743
837
  });
744
838
  }
745
- for (const [filename, content] of Object.entries(SKILLS)) {
746
- const skillName = filename.replace(/\.md$/, "");
747
- const skillDir = join4(skillsDir, skillName);
748
- ensureDir(skillDir);
749
- writeFile(join4(skillDir, "SKILL.md"), content, opts.force, result);
839
+ if (wants("claude")) {
840
+ for (const [filename, content] of Object.entries(SKILLS)) {
841
+ const skillName = filename.replace(/\.md$/, "");
842
+ const skillDir = join5(skillsDir, skillName);
843
+ ensureDir(skillDir);
844
+ writeFile(join5(skillDir, "SKILL.md"), content, opts.force, result);
845
+ }
750
846
  }
751
- const codexSkillsDir = join4(targetDir, ".agents", "skills");
752
- let existingCodexSkills = [];
753
- if (existsSync4(codexSkillsDir)) {
754
- existingCodexSkills = readdirSync2(codexSkillsDir).filter((name) => {
755
- if (name.startsWith("joycraft-")) return false;
756
- if (name.startsWith(".")) return false;
757
- const fullPath = join4(codexSkillsDir, name);
758
- try {
759
- return statSync(fullPath).isDirectory();
760
- } catch {
761
- return false;
762
- }
763
- });
847
+ if (wants("codex")) {
848
+ const codexSkillsDir = join5(targetDir, ".agents", "skills");
849
+ for (const [filename, content] of Object.entries(CODEX_SKILLS)) {
850
+ const skillName = filename.replace(/\.md$/, "");
851
+ const skillDir = join5(codexSkillsDir, skillName);
852
+ ensureDir(skillDir);
853
+ writeFile(join5(skillDir, "SKILL.md"), content, opts.force, result);
854
+ }
764
855
  }
765
- for (const [filename, content] of Object.entries(CODEX_SKILLS)) {
766
- const skillName = filename.replace(/\.md$/, "");
767
- const skillDir = join4(codexSkillsDir, skillName);
768
- ensureDir(skillDir);
769
- writeFile(join4(skillDir, "SKILL.md"), content, opts.force, result);
770
- }
771
- const piSkillsDir = join4(targetDir, ".pi", "skills");
772
- for (const [filename, content] of Object.entries(PI_SKILLS)) {
773
- const skillName = filename.replace(/\.md$/, "");
774
- const skillDir = join4(piSkillsDir, skillName);
775
- ensureDir(skillDir);
776
- writeFile(join4(skillDir, "SKILL.md"), content, opts.force, result);
777
- }
778
- const piScriptsDir = join4(targetDir, ".pi", "scripts", "joycraft");
779
- ensureDir(piScriptsDir);
780
- for (const [name, content] of Object.entries(PI_SCRIPTS)) {
781
- const scriptPath = join4(piScriptsDir, name);
782
- writeFile(scriptPath, content, opts.force, result);
783
- if (name !== "README.md") {
784
- try {
785
- chmodSync(scriptPath, 493);
786
- } catch {
856
+ if (wants("pi")) {
857
+ const piSkillsDir = join5(targetDir, ".pi", "skills");
858
+ for (const [filename, content] of Object.entries(PI_SKILLS)) {
859
+ const skillName = filename.replace(/\.md$/, "");
860
+ const skillDir = join5(piSkillsDir, skillName);
861
+ ensureDir(skillDir);
862
+ writeFile(join5(skillDir, "SKILL.md"), content, opts.force, result);
863
+ }
864
+ const piScriptsDir = join5(targetDir, ".pi", "scripts", "joycraft");
865
+ ensureDir(piScriptsDir);
866
+ for (const [name, content] of Object.entries(PI_SCRIPTS)) {
867
+ const scriptPath = join5(piScriptsDir, name);
868
+ writeFile(scriptPath, content, opts.force, result);
869
+ if (name !== "README.md") {
870
+ try {
871
+ chmodSync(scriptPath, 493);
872
+ } catch {
873
+ }
787
874
  }
788
875
  }
876
+ const piExtDir = join5(targetDir, ".pi", "extensions");
877
+ ensureDir(piExtDir);
878
+ for (const [name, content] of Object.entries(PI_EXTENSIONS)) {
879
+ writeFile(join5(piExtDir, name), content, opts.force, result);
880
+ }
881
+ const piAgentsDir = join5(targetDir, ".pi", "agents");
882
+ ensureDir(piAgentsDir);
883
+ for (const [name, content] of Object.entries(PI_AGENTS)) {
884
+ writeFile(join5(piAgentsDir, name), content, opts.force, result);
885
+ }
789
886
  }
790
- const piExtDir = join4(targetDir, ".pi", "extensions");
791
- ensureDir(piExtDir);
792
- for (const [name, content] of Object.entries(PI_EXTENSIONS)) {
793
- writeFile(join4(piExtDir, name), content, opts.force, result);
794
- }
795
- const piAgentsDir = join4(targetDir, ".pi", "agents");
796
- ensureDir(piAgentsDir);
797
- for (const [name, content] of Object.entries(PI_AGENTS)) {
798
- writeFile(join4(piAgentsDir, name), content, opts.force, result);
799
- }
800
- const templatesDir = join4(targetDir, "docs", "templates");
887
+ const templatesDir = join5(targetDir, "docs", "templates");
801
888
  ensureDir(templatesDir);
802
889
  for (const [filename, content] of Object.entries(TEMPLATES)) {
803
- ensureDir(dirname(join4(templatesDir, filename)));
804
- writeFile(join4(templatesDir, filename), content, opts.force, result);
890
+ ensureDir(dirname(join5(templatesDir, filename)));
891
+ writeFile(join5(templatesDir, filename), content, opts.force, result);
805
892
  }
806
- const claudeMdPath = join4(targetDir, "CLAUDE.md");
807
- if (existsSync4(claudeMdPath) && !opts.force) {
893
+ const claudeMdPath = join5(targetDir, "CLAUDE.md");
894
+ if (existsSync5(claudeMdPath) && !opts.force) {
808
895
  result.skipped.push(claudeMdPath);
809
896
  } else {
810
897
  const projectName = basename(targetDir);
811
- const content = generateCLAUDEMd(projectName, stack, existingSkills);
812
- writeFileSync2(claudeMdPath, content, "utf-8");
898
+ const content = generateCLAUDEMd(projectName, stack, existingSkills, {
899
+ privateProfile: gitignoreProfile === "private"
900
+ });
901
+ writeFileSync3(claudeMdPath, content, "utf-8");
813
902
  result.created.push(claudeMdPath);
814
903
  }
815
- const agentsMdPath = join4(targetDir, "AGENTS.md");
816
- if (existsSync4(agentsMdPath) && !opts.force) {
904
+ const agentsMdPath = join5(targetDir, "AGENTS.md");
905
+ if (existsSync5(agentsMdPath) && !opts.force) {
817
906
  result.skipped.push(agentsMdPath);
818
907
  } else {
819
908
  const projectName = basename(targetDir);
820
- const content = generateAgentsMd(projectName, stack);
821
- writeFileSync2(agentsMdPath, content, "utf-8");
909
+ const content = generateAgentsMd(projectName, stack, gitignoreProfile === "private");
910
+ writeFileSync3(agentsMdPath, content, "utf-8");
822
911
  result.created.push(agentsMdPath);
823
912
  }
824
913
  const fileHashes = {};
825
- for (const [filename, content] of Object.entries(SKILLS)) {
826
- const skillName = filename.replace(/\.md$/, "");
827
- fileHashes[join4(".claude", "skills", skillName, "SKILL.md")] = hashContent(content);
914
+ if (wants("claude")) {
915
+ for (const [filename, content] of Object.entries(SKILLS)) {
916
+ const skillName = filename.replace(/\.md$/, "");
917
+ fileHashes[join5(".claude", "skills", skillName, "SKILL.md")] = hashContent(content);
918
+ }
828
919
  }
829
- for (const [filename, content] of Object.entries(CODEX_SKILLS)) {
830
- const skillName = filename.replace(/\.md$/, "");
831
- fileHashes[join4(".agents", "skills", skillName, "SKILL.md")] = hashContent(content);
920
+ if (wants("codex")) {
921
+ for (const [filename, content] of Object.entries(CODEX_SKILLS)) {
922
+ const skillName = filename.replace(/\.md$/, "");
923
+ fileHashes[join5(".agents", "skills", skillName, "SKILL.md")] = hashContent(content);
924
+ }
832
925
  }
833
926
  for (const [filename, content] of Object.entries(TEMPLATES)) {
834
- fileHashes[join4("docs", "templates", filename)] = hashContent(content);
835
- }
836
- for (const [filename, content] of Object.entries(PI_SKILLS)) {
837
- const skillName = filename.replace(/\.md$/, "");
838
- fileHashes[join4(".pi", "skills", skillName, "SKILL.md")] = hashContent(content);
839
- }
840
- for (const [name, content] of Object.entries(PI_SCRIPTS)) {
841
- fileHashes[join4(".pi", "scripts", "joycraft", name)] = hashContent(content);
927
+ fileHashes[join5("docs", "templates", filename)] = hashContent(content);
842
928
  }
843
- for (const [name, content] of Object.entries(PI_EXTENSIONS)) {
844
- fileHashes[join4(".pi", "extensions", name)] = hashContent(content);
845
- }
846
- for (const [name, content] of Object.entries(PI_AGENTS)) {
847
- fileHashes[join4(".pi", "agents", name)] = hashContent(content);
929
+ if (wants("pi")) {
930
+ for (const [filename, content] of Object.entries(PI_SKILLS)) {
931
+ const skillName = filename.replace(/\.md$/, "");
932
+ fileHashes[join5(".pi", "skills", skillName, "SKILL.md")] = hashContent(content);
933
+ }
934
+ for (const [name, content] of Object.entries(PI_SCRIPTS)) {
935
+ fileHashes[join5(".pi", "scripts", "joycraft", name)] = hashContent(content);
936
+ }
937
+ for (const [name, content] of Object.entries(PI_EXTENSIONS)) {
938
+ fileHashes[join5(".pi", "extensions", name)] = hashContent(content);
939
+ }
940
+ for (const [name, content] of Object.entries(PI_AGENTS)) {
941
+ fileHashes[join5(".pi", "agents", name)] = hashContent(content);
942
+ }
848
943
  }
849
- writeVersion(targetDir, getPackageVersion(), fileHashes, gitignoreProfile);
944
+ writeVersion(targetDir, getPackageVersion(), fileHashes, gitignoreProfile, harnesses);
850
945
  applyGitignoreProfile(targetDir, gitignoreProfile);
851
- const hooksDir = join4(targetDir, ".claude", "hooks");
852
- ensureDir(hooksDir);
853
- const hookScript = `// Joycraft version check \u2014 runs on Claude Code session start
946
+ if (wants("pi")) {
947
+ const outcome = ensurePiExcludedFromTsconfig(targetDir);
948
+ if (outcome.status === "added") {
949
+ result.modified.push(outcome.path);
950
+ result.warnings.push(
951
+ 'Added ".pi" to tsconfig.json "exclude" so the Pi extension stays out of your TypeScript build.'
952
+ );
953
+ } else if (outcome.status === "skipped") {
954
+ result.warnings.push(outcome.reason);
955
+ }
956
+ }
957
+ if (wants("claude")) {
958
+ const hooksDir = join5(targetDir, ".claude", "hooks");
959
+ ensureDir(hooksDir);
960
+ const hookScript = `// Joycraft version check \u2014 runs on Claude Code session start
854
961
  import { readFileSync } from 'node:fs';
855
962
  import { join } from 'node:path';
856
963
  try {
@@ -862,72 +969,81 @@ try {
862
969
  }
863
970
  } catch {}
864
971
  `;
865
- writeFile(join4(hooksDir, "joycraft-version-check.mjs"), hookScript, opts.force, result);
866
- const settingsPath = join4(targetDir, ".claude", "settings.json");
867
- let settings = {};
868
- let settingsMalformed = false;
869
- if (existsSync4(settingsPath)) {
870
- try {
871
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
872
- } catch {
873
- settingsMalformed = true;
874
- result.warnings.push(
875
- "settings.json exists but is malformed \u2014 skipping settings merge to protect your config.\n Fix the JSON in .claude/settings.json and re-run init."
876
- );
877
- }
878
- }
879
- if (!settingsMalformed) {
880
- if (!settings.hooks) settings.hooks = {};
881
- const hooksConfig = settings.hooks;
882
- if (!hooksConfig.SessionStart) hooksConfig.SessionStart = [];
883
- const sessionStartHooks = hooksConfig.SessionStart;
884
- const hasJoycraftHook = sessionStartHooks.some((h) => {
885
- const innerHooks = h.hooks;
886
- return innerHooks?.some((ih) => typeof ih.command === "string" && ih.command.includes("joycraft"));
887
- });
888
- if (!hasJoycraftHook) {
889
- sessionStartHooks.push({
890
- matcher: "",
891
- hooks: [{
892
- type: "command",
893
- command: "node .claude/hooks/joycraft-version-check.mjs"
894
- }]
895
- });
896
- writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
897
- result.created.push(settingsPath);
898
- }
899
- const permissions = generatePermissions(stack);
900
- if (existsSync4(settingsPath)) {
972
+ writeFile(join5(hooksDir, "joycraft-version-check.mjs"), hookScript, opts.force, result);
973
+ const settingsPath = join5(targetDir, ".claude", "settings.json");
974
+ let settings = {};
975
+ let settingsMalformed = false;
976
+ if (existsSync5(settingsPath)) {
901
977
  try {
902
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
978
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
903
979
  } catch {
980
+ settingsMalformed = true;
904
981
  result.warnings.push(
905
- "settings.json became unreadable after hook merge \u2014 skipping permissions merge.\n Fix the JSON in .claude/settings.json and re-run init."
982
+ "settings.json exists but is malformed \u2014 skipping settings merge to protect your config.\n Fix the JSON in .claude/settings.json and re-run init."
906
983
  );
907
- settingsMalformed = true;
908
984
  }
909
985
  }
910
986
  if (!settingsMalformed) {
911
- if (!settings.permissions) settings.permissions = {};
912
- const perms = settings.permissions;
913
- if (!perms.allow) perms.allow = [];
914
- if (!perms.deny) perms.deny = [];
915
- for (const rule of permissions.allow) {
916
- if (!perms.allow.includes(rule)) perms.allow.push(rule);
987
+ if (!settings.hooks) settings.hooks = {};
988
+ const hooksConfig = settings.hooks;
989
+ if (!hooksConfig.SessionStart) hooksConfig.SessionStart = [];
990
+ const sessionStartHooks = hooksConfig.SessionStart;
991
+ const hasJoycraftHook = sessionStartHooks.some((h) => {
992
+ const innerHooks = h.hooks;
993
+ return innerHooks?.some((ih) => typeof ih.command === "string" && ih.command.includes("joycraft"));
994
+ });
995
+ if (!settings.env) settings.env = {};
996
+ const env = settings.env;
997
+ const envMissing = !("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" in env);
998
+ if (envMissing) {
999
+ env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
1000
+ }
1001
+ if (!hasJoycraftHook) {
1002
+ sessionStartHooks.push({
1003
+ matcher: "",
1004
+ hooks: [{
1005
+ type: "command",
1006
+ command: "node .claude/hooks/joycraft-version-check.mjs"
1007
+ }]
1008
+ });
1009
+ }
1010
+ if (!hasJoycraftHook || envMissing) {
1011
+ writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1012
+ if (!result.created.includes(settingsPath)) result.created.push(settingsPath);
917
1013
  }
918
- for (const rule of permissions.deny) {
919
- if (!perms.deny.includes(rule)) perms.deny.push(rule);
1014
+ const permissions = generatePermissions(stack);
1015
+ if (existsSync5(settingsPath)) {
1016
+ try {
1017
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1018
+ } catch {
1019
+ result.warnings.push(
1020
+ "settings.json became unreadable after hook merge \u2014 skipping permissions merge.\n Fix the JSON in .claude/settings.json and re-run init."
1021
+ );
1022
+ settingsMalformed = true;
1023
+ }
1024
+ }
1025
+ if (!settingsMalformed) {
1026
+ if (!settings.permissions) settings.permissions = {};
1027
+ const perms = settings.permissions;
1028
+ if (!perms.allow) perms.allow = [];
1029
+ if (!perms.deny) perms.deny = [];
1030
+ for (const rule of permissions.allow) {
1031
+ if (!perms.allow.includes(rule)) perms.allow.push(rule);
1032
+ }
1033
+ for (const rule of permissions.deny) {
1034
+ if (!perms.deny.includes(rule)) perms.deny.push(rule);
1035
+ }
1036
+ writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
920
1037
  }
921
- writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
922
1038
  }
1039
+ const hookResult = installSafeguardHooks(targetDir, [], opts.force, settingsMalformed);
1040
+ result.created.push(...hookResult.created);
1041
+ result.skipped.push(...hookResult.skipped);
923
1042
  }
924
- const hookResult = installSafeguardHooks(targetDir, [], opts.force, settingsMalformed);
925
- result.created.push(...hookResult.created);
926
- result.skipped.push(...hookResult.skipped);
927
- if (gitignoreProfile === "shared") {
928
- const gitignorePath = join4(targetDir, ".gitignore");
929
- if (existsSync4(gitignorePath)) {
930
- const gitignore = readFileSync3(gitignorePath, "utf-8");
1043
+ if (gitignoreProfile === "shared" && wants("claude")) {
1044
+ const gitignorePath = join5(targetDir, ".gitignore");
1045
+ if (existsSync5(gitignorePath)) {
1046
+ const gitignore = readFileSync4(gitignorePath, "utf-8");
931
1047
  if (/^\.claude\/?$/m.test(gitignore) || /^\.claude\/\*$/m.test(gitignore)) {
932
1048
  result.warnings.push(
933
1049
  ".claude/ is in your .gitignore \u2014 teammates won't get Joycraft skills.\n Add this line to .gitignore to fix: !.claude/skills/"
@@ -935,10 +1051,13 @@ try {
935
1051
  }
936
1052
  }
937
1053
  }
938
- printSummary(result, stack, existingSkills, isPi, gitignoreProfile);
1054
+ printSummary(result, stack, existingSkills, isPi, gitignoreProfile, harnesses);
939
1055
  }
940
- function printSummary(result, stack, existingSkills = [], isPi = false, gitignoreProfile = DEFAULT_GITIGNORE_PROFILE) {
1056
+ function printSummary(result, stack, existingSkills = [], isPi = false, gitignoreProfile = DEFAULT_GITIGNORE_PROFILE, harnesses = []) {
941
1057
  console.log("\nJoycraft initialized!\n");
1058
+ if (harnesses.length > 0) {
1059
+ console.log(` Installed harnesses: ${harnesses.join(", ")}`);
1060
+ }
942
1061
  if (stack.language !== "unknown") {
943
1062
  const fw = stack.framework ? ` + ${stack.framework}` : "";
944
1063
  console.log(` Detected stack: ${stack.language}${fw} (${stack.packageManager})`);
@@ -1008,4 +1127,4 @@ function printSummary(result, stack, existingSkills = [], isPi = false, gitignor
1008
1127
  export {
1009
1128
  init
1010
1129
  };
1011
- //# sourceMappingURL=init-OUKVQXNY.js.map
1130
+ //# sourceMappingURL=init-WPKDBQDN.js.map