joycraft 0.6.15 → 0.6.17
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/README.md +33 -13
- package/dist/{chunk-XOMQIK4U.js → chunk-3JVMOJTA.js} +9 -187
- package/dist/chunk-3JVMOJTA.js.map +1 -0
- package/dist/{chunk-YE4LWG2O.js → chunk-GWDLD7SH.js} +3 -2
- package/dist/chunk-GWDLD7SH.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/{init-LXSMLAY5.js → init-OO77DURX.js} +163 -58
- package/dist/init-OO77DURX.js.map +1 -0
- package/dist/{init-autofix-OZW5ITFI.js → init-autofix-6GMXHF5F.js} +5 -3
- package/dist/{init-autofix-OZW5ITFI.js.map → init-autofix-6GMXHF5F.js.map} +1 -1
- package/dist/{upgrade-P3JZS7NM.js → upgrade-VKQ54KRD.js} +31 -6
- package/dist/upgrade-VKQ54KRD.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-XOMQIK4U.js.map +0 -1
- package/dist/chunk-YE4LWG2O.js.map +0 -1
- package/dist/init-LXSMLAY5.js.map +0 -1
- package/dist/upgrade-P3JZS7NM.js.map +0 -1
|
@@ -24,7 +24,7 @@ function ensureGitignoreEntries(targetDir, entries) {
|
|
|
24
24
|
}
|
|
25
25
|
function applyGitignoreProfile(targetDir, profile) {
|
|
26
26
|
if (profile === "private") {
|
|
27
|
-
return ensureGitignoreEntries(targetDir, PRIVATE_PROFILE_IGNORES);
|
|
27
|
+
return ensureGitignoreEntries(targetDir, [...PRIVATE_PROFILE_IGNORES, STATE_PATH]);
|
|
28
28
|
}
|
|
29
29
|
return ensureGitignoreEntries(targetDir, [STATE_PATH]);
|
|
30
30
|
}
|
|
@@ -77,10 +77,11 @@ async function resolveGitignoreProfile(opts) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export {
|
|
80
|
+
PRIVATE_PROFILE_IGNORES,
|
|
80
81
|
PRIVATE_DIRS_DISPLAY,
|
|
81
82
|
PRIVATE_UNTRACK_COMMAND,
|
|
82
83
|
applyGitignoreProfile,
|
|
83
84
|
validateGitignoreFlag,
|
|
84
85
|
resolveGitignoreProfile
|
|
85
86
|
};
|
|
86
|
-
//# sourceMappingURL=chunk-
|
|
87
|
+
//# sourceMappingURL=chunk-GWDLD7SH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/gitignore.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport {\n STATE_PATH,\n parseGitignoreProfile,\n DEFAULT_GITIGNORE_PROFILE,\n type GitignoreProfile,\n} from './version.js';\n\n/**\n * The harness directories the `private` profile gitignores. Tracking only\n * CLAUDE.md, AGENTS.md, and docs/ means everything under these three dirs\n * stays local. `.claude/` already covers the hidden state file.\n *\n * Single source of truth: every user-facing string that names these dirs\n * (prompts, summaries, the untrack hint, CLI help) derives from this list via\n * the constants below, so adding a harness dir can't leave stale messages.\n */\nexport const PRIVATE_PROFILE_IGNORES = ['.claude/', '.agents/', '.pi/'];\n\n/** Human-readable list of the private-profile dirs, for prompts and summaries. */\nexport const PRIVATE_DIRS_DISPLAY = PRIVATE_PROFILE_IGNORES.join(', ');\n\n/** Copy-pasteable command to untrack already-committed harness files. */\nexport const PRIVATE_UNTRACK_COMMAND = `git rm -r --cached ${PRIVATE_PROFILE_IGNORES.map((d) => d.replace(/\\/$/, '')).join(' ')}`;\n\n/**\n * Append-only, create-if-absent, idempotent .gitignore writer.\n *\n * Mirrors the \"append over modify when touching user files\" principle: it never\n * rewrites, reorders, or removes existing lines — it only appends the entries\n * not already present (matched exactly, after trimming). One read + at most one\n * write per call. Returns the entries actually added (empty when everything was\n * already present).\n */\nexport function ensureGitignoreEntries(targetDir: string, entries: string[]): string[] {\n const gitignorePath = join(targetDir, '.gitignore');\n const current = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';\n const present = new Set(current.split('\\n').map((l) => l.trim()));\n const missing = entries.filter((e) => !present.has(e.trim()));\n if (missing.length === 0) return [];\n\n // Append on their own lines, tolerating a file that may or may not end in \\n.\n const sep = current.length > 0 && !current.endsWith('\\n') ? '\\n' : '';\n writeFileSync(gitignorePath, current + sep + missing.join('\\n') + '\\n', 'utf-8');\n return missing;\n}\n\n/** Single-entry convenience wrapper around ensureGitignoreEntries. */\nexport function ensureGitignoreEntry(targetDir: string, line: string): boolean {\n return ensureGitignoreEntries(targetDir, [line]).length > 0;\n}\n\n/**\n * Apply a gitignore profile's entries to the project's .gitignore.\n *\n * The hidden upgrade-state file (`STATE_PATH`) is tool-managed, regenerated on\n * every init/upgrade, and must never be committed — under BOTH profiles. It now\n * lives at `docs/.joycraft/state.json`; since `docs/` is always tracked, the\n * state entry is no longer covered transitively by any harness-dir ignore, so\n * both profiles list it explicitly.\n *\n * - `shared` — ignore only the hidden state file (commit the harness dirs).\n * - `private` — ignore the .claude/, .agents/, .pi/ trees AND the state file.\n *\n * Append-only and idempotent (via ensureGitignoreEntries), so re-running\n * init/upgrade never duplicates entries. Returns the list of lines actually\n * added this call.\n */\nexport function applyGitignoreProfile(targetDir: string, profile: GitignoreProfile): string[] {\n if (profile === 'private') {\n return ensureGitignoreEntries(targetDir, [...PRIVATE_PROFILE_IGNORES, STATE_PATH]);\n }\n // `shared`: only the hidden state file, matching long-standing behavior.\n return ensureGitignoreEntries(targetDir, [STATE_PATH]);\n}\n\n/** A resolved profile plus how it was arrived at. */\nexport interface ResolvedGitignoreProfile {\n profile: GitignoreProfile;\n /**\n * True when the profile is an actual decision: a --gitignore flag, a\n * persisted choice, or an interactive answer. False when it is the\n * non-interactive fallback default — callers must NOT persist a fallback,\n * or the one-time prompt would be permanently suppressed for the project.\n */\n decided: boolean;\n}\n\n/** Validate a raw --gitignore flag value. Throws the user-facing error on unknown values. */\nexport function validateGitignoreFlag(flag: string): GitignoreProfile {\n const parsed = parseGitignoreProfile(flag);\n if (!parsed) {\n throw new Error(`Unknown gitignore profile '${flag}'. Use 'shared' or 'private'.`);\n }\n return parsed;\n}\n\n/**\n * Prompt for a gitignore profile. An empty answer takes the default; an\n * unrecognized answer re-asks instead of being silently coerced — a typo must\n * not get persisted as a permanent choice.\n */\nasync function promptGitignoreProfile(intro: string): Promise<GitignoreProfile> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n console.log(intro);\n console.log(' shared — commit skills so your team gets the same workflow (default)');\n console.log(` private — gitignore ${PRIVATE_DIRS_DISPLAY}; track only CLAUDE.md, AGENTS.md, docs/`);\n return new Promise((resolve) => {\n const ask = (): void => {\n rl.question(`Profile [shared/private] (${DEFAULT_GITIGNORE_PROFILE}): `, (answer) => {\n if (answer.trim() === '') {\n rl.close();\n resolve(DEFAULT_GITIGNORE_PROFILE);\n return;\n }\n const parsed = parseGitignoreProfile(answer);\n if (parsed) {\n rl.close();\n resolve(parsed);\n return;\n }\n console.log(\n `Unrecognized answer '${answer.trim()}' — type 'shared' or 'private', or press Enter for ${DEFAULT_GITIGNORE_PROFILE}.`\n );\n ask();\n });\n };\n ask();\n });\n}\n\n/**\n * Resolve the gitignore profile by precedence — the single resolver shared by\n * init and upgrade so the two commands can never drift:\n * 1. --gitignore flag (validated; throws on unknown value)\n * 2. profile persisted in state.json (re-init / upgrade keep the prior choice)\n * 3. interactive prompt (the caller decides when prompting is allowed)\n * 4. fallback default `shared`, reported with decided=false\n */\nexport async function resolveGitignoreProfile(opts: {\n /** Raw --gitignore value from the CLI, if provided. */\n flag?: string;\n /** Profile previously persisted in state.json, if any. */\n persisted?: GitignoreProfile;\n /** Whether prompting is allowed (TTY, and the command permits interaction). */\n interactive: boolean;\n /** First line printed above the prompt; init and upgrade word it differently. */\n promptIntro: string;\n}): Promise<ResolvedGitignoreProfile> {\n if (opts.flag !== undefined) {\n return { profile: validateGitignoreFlag(opts.flag), decided: true };\n }\n if (opts.persisted) {\n return { profile: opts.persisted, decided: true };\n }\n if (opts.interactive) {\n return { profile: await promptGitignoreProfile(opts.promptIntro), decided: true };\n }\n return { profile: DEFAULT_GITIGNORE_PROFILE, decided: false };\n}\n"],"mappings":";;;;;;;;AAAA,SAAS,YAAY,cAAc,qBAAqB;AACxD,SAAS,YAAY;AACrB,SAAS,uBAAuB;AAiBzB,IAAM,0BAA0B,CAAC,YAAY,YAAY,MAAM;AAG/D,IAAM,uBAAuB,wBAAwB,KAAK,IAAI;AAG9D,IAAM,0BAA0B,sBAAsB,wBAAwB,IAAI,CAAC,MAAM,EAAE,QAAQ,OAAO,EAAE,CAAC,EAAE,KAAK,GAAG,CAAC;AAWxH,SAAS,uBAAuB,WAAmB,SAA6B;AACrF,QAAM,gBAAgB,KAAK,WAAW,YAAY;AAClD,QAAM,UAAU,WAAW,aAAa,IAAI,aAAa,eAAe,OAAO,IAAI;AACnF,QAAM,UAAU,IAAI,IAAI,QAAQ,MAAM,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAChE,QAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,KAAK,CAAC,CAAC;AAC5D,MAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAGlC,QAAM,MAAM,QAAQ,SAAS,KAAK,CAAC,QAAQ,SAAS,IAAI,IAAI,OAAO;AACnE,gBAAc,eAAe,UAAU,MAAM,QAAQ,KAAK,IAAI,IAAI,MAAM,OAAO;AAC/E,SAAO;AACT;AAuBO,SAAS,sBAAsB,WAAmB,SAAqC;AAC5F,MAAI,YAAY,WAAW;AACzB,WAAO,uBAAuB,WAAW,CAAC,GAAG,yBAAyB,UAAU,CAAC;AAAA,EACnF;AAEA,SAAO,uBAAuB,WAAW,CAAC,UAAU,CAAC;AACvD;AAeO,SAAS,sBAAsB,MAAgC;AACpE,QAAM,SAAS,sBAAsB,IAAI;AACzC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,8BAA8B,IAAI,+BAA+B;AAAA,EACnF;AACA,SAAO;AACT;AAOA,eAAe,uBAAuB,OAA0C;AAC9E,QAAM,KAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAC3E,UAAQ,IAAI,KAAK;AACjB,UAAQ,IAAI,8EAAyE;AACrF,UAAQ,IAAI,8BAAyB,oBAAoB,0CAA0C;AACnG,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,MAAM,MAAY;AACtB,SAAG,SAAS,6BAA6B,yBAAyB,OAAO,CAAC,WAAW;AACnF,YAAI,OAAO,KAAK,MAAM,IAAI;AACxB,aAAG,MAAM;AACT,kBAAQ,yBAAyB;AACjC;AAAA,QACF;AACA,cAAM,SAAS,sBAAsB,MAAM;AAC3C,YAAI,QAAQ;AACV,aAAG,MAAM;AACT,kBAAQ,MAAM;AACd;AAAA,QACF;AACA,gBAAQ;AAAA,UACN,wBAAwB,OAAO,KAAK,CAAC,2DAAsD,yBAAyB;AAAA,QACtH;AACA,YAAI;AAAA,MACN,CAAC;AAAA,IACH;AACA,QAAI;AAAA,EACN,CAAC;AACH;AAUA,eAAsB,wBAAwB,MASR;AACpC,MAAI,KAAK,SAAS,QAAW;AAC3B,WAAO,EAAE,SAAS,sBAAsB,KAAK,IAAI,GAAG,SAAS,KAAK;AAAA,EACpE;AACA,MAAI,KAAK,WAAW;AAClB,WAAO,EAAE,SAAS,KAAK,WAAW,SAAS,KAAK;AAAA,EAClD;AACA,MAAI,KAAK,aAAa;AACpB,WAAO,EAAE,SAAS,MAAM,uBAAuB,KAAK,WAAW,GAAG,SAAS,KAAK;AAAA,EAClF;AACA,SAAO,EAAE,SAAS,2BAA2B,SAAS,MAAM;AAC9D;","names":[]}
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
PRIVATE_DIRS_DISPLAY
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GWDLD7SH.js";
|
|
5
5
|
import "./chunk-34IWIKXS.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
@@ -15,7 +15,7 @@ var GITIGNORE_OPTION_DESC = `Gitignore profile: 'shared' (commit skills) or 'pri
|
|
|
15
15
|
var program = new Command();
|
|
16
16
|
program.name("joycraft").description("Scaffold and upgrade AI development harnesses").version(pkg.version, "-v, --version");
|
|
17
17
|
program.command("init").description("Scaffold the Joycraft harness into the current project").argument("[dir]", "Target directory", ".").option("--force", "Overwrite existing files").option("--gitignore <profile>", GITIGNORE_OPTION_DESC).action(async (dir, opts) => {
|
|
18
|
-
const { init } = await import("./init-
|
|
18
|
+
const { init } = await import("./init-OO77DURX.js");
|
|
19
19
|
try {
|
|
20
20
|
await init(dir, { force: opts.force ?? false, gitignore: opts.gitignore });
|
|
21
21
|
} catch (err) {
|
|
@@ -24,7 +24,7 @@ program.command("init").description("Scaffold the Joycraft harness into the curr
|
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
program.command("upgrade").description("Upgrade installed Joycraft templates and skills to latest").argument("[dir]", "Target directory", ".").option("--yes", "Auto-accept all updates").option("--gitignore <profile>", GITIGNORE_OPTION_DESC).action(async (dir, opts) => {
|
|
27
|
-
const { upgrade } = await import("./upgrade-
|
|
27
|
+
const { upgrade } = await import("./upgrade-VKQ54KRD.js");
|
|
28
28
|
try {
|
|
29
29
|
await upgrade(dir, { yes: opts.yes ?? false, gitignore: opts.gitignore });
|
|
30
30
|
} catch (err) {
|
|
@@ -33,7 +33,7 @@ program.command("upgrade").description("Upgrade installed Joycraft templates and
|
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
35
|
program.command("init-autofix").description("Set up the Level 5 auto-fix loop with holdout scenarios").argument("[dir]", "Target directory", ".").option("--scenarios-repo <name>", "Name for scenarios repo").option("--app-id <id>", "GitHub App ID for Joycraft Autofix").option("--force", "Overwrite existing workflow files").option("--dry-run", "Show what would be created without creating it").action(async (dir, opts) => {
|
|
36
|
-
const { initAutofix } = await import("./init-autofix-
|
|
36
|
+
const { initAutofix } = await import("./init-autofix-6GMXHF5F.js");
|
|
37
37
|
await initAutofix(dir, opts);
|
|
38
38
|
});
|
|
39
39
|
program.command("check-version").description("Check if a newer version of Joycraft is available").action(async () => {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
PRIVATE_UNTRACK_COMMAND,
|
|
8
8
|
applyGitignoreProfile,
|
|
9
9
|
resolveGitignoreProfile
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-GWDLD7SH.js";
|
|
11
11
|
import {
|
|
12
12
|
CODEX_SKILLS,
|
|
13
13
|
PI_AGENTS,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
PI_SKILLS,
|
|
17
17
|
SKILLS,
|
|
18
18
|
TEMPLATES
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-3JVMOJTA.js";
|
|
20
20
|
import {
|
|
21
21
|
DEFAULT_GITIGNORE_PROFILE,
|
|
22
22
|
STATE_PATH,
|
|
@@ -27,8 +27,8 @@ import {
|
|
|
27
27
|
} from "./chunk-34IWIKXS.js";
|
|
28
28
|
|
|
29
29
|
// src/init.ts
|
|
30
|
-
import { mkdirSync as mkdirSync2, existsSync as
|
|
31
|
-
import { join as
|
|
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";
|
|
32
32
|
|
|
33
33
|
// src/detect.ts
|
|
34
34
|
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
@@ -313,7 +313,7 @@ import { existsSync as existsSync2 } from "fs";
|
|
|
313
313
|
import { join as join2 } from "path";
|
|
314
314
|
var PRIVATE_SETUP_NOTE_MARKER = "After cloning, run";
|
|
315
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.`;
|
|
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 \u2014 it only creates missing files and leaves your committed \`CLAUDE.md\`, \`AGENTS.md\`, and \`docs/\` untouched (use \`--force\` only if you deliberately want to regenerate them).`;
|
|
317
317
|
}
|
|
318
318
|
function generateCommandsBlock(stack) {
|
|
319
319
|
const lines = ["```bash"];
|
|
@@ -365,7 +365,9 @@ ${generateCommandsBlock(stack)}
|
|
|
365
365
|
|
|
366
366
|
**Default execution mode:** batch
|
|
367
367
|
|
|
368
|
-
_How \`/joycraft-implement\` wraps up after each spec. \`joycraft-decompose\` reads this line (absent \u21D2 \`batch\`) and recommends a per-spec mode you approve. Modes: \`batch\` (implement a cluster, wrap once at the end), \`checkpoint\` (commit + status bump after each spec), \`isolated\` (fresh context per spec \u2014 on Pi, the \`joycraft-implement-loop\` driver). Change the value above to set your project default._
|
|
368
|
+
_How \`/joycraft-implement\` wraps up after each spec. \`joycraft-decompose\` reads this line (absent \u21D2 \`batch\`) and recommends a per-spec mode you approve. Modes: \`batch\` (implement a cluster, wrap once at the end), \`checkpoint\` (commit + status bump after each spec), \`isolated\` (fresh context per spec \u2014 on Pi, the \`joycraft-implement-loop\` driver). Change the value above to set your project default._
|
|
369
|
+
|
|
370
|
+
**Deferred work \u2192 \`docs/backlog/\`.** Ideas and follow-ups you surface mid-sprint but can't take on now go to \`docs/backlog/\` (one file per item) so the current spec stays focused without losing the thread. Promote an entry to a Feature Brief under \`docs/features/<slug>/\` when you're ready to build it.`;
|
|
369
371
|
}
|
|
370
372
|
function generateArchitectureSection() {
|
|
371
373
|
return `## Architecture
|
|
@@ -487,7 +489,9 @@ function generateExternalApiSafetySection() {
|
|
|
487
489
|
function generateDevelopmentSection(stack) {
|
|
488
490
|
return `## Development
|
|
489
491
|
|
|
490
|
-
${generateCommandsBlock2(stack)}
|
|
492
|
+
${generateCommandsBlock2(stack)}
|
|
493
|
+
|
|
494
|
+
**Deferred work \u2192 \`docs/backlog/\`.** Ideas and follow-ups surfaced mid-sprint that can't be taken on now go to \`docs/backlog/\` (one file per item) so the current spec stays focused. Promote an entry to a Feature Brief under \`docs/features/<slug>/\` when ready to build it.`;
|
|
491
495
|
}
|
|
492
496
|
function generateArchitectureSection2() {
|
|
493
497
|
return `## Architecture
|
|
@@ -713,25 +717,113 @@ function installSafeguardHooks(targetDir, customPatterns = [], force = false, sk
|
|
|
713
717
|
return result;
|
|
714
718
|
}
|
|
715
719
|
|
|
720
|
+
// src/tsconfig.ts
|
|
721
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
722
|
+
import { join as join4 } from "path";
|
|
723
|
+
var PI_EXCLUDE = ".pi";
|
|
724
|
+
function stripJsonComments(text) {
|
|
725
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
726
|
+
}
|
|
727
|
+
function alreadyExcludesPi(rawText) {
|
|
728
|
+
try {
|
|
729
|
+
const parsed = JSON.parse(stripJsonComments(rawText));
|
|
730
|
+
if (!Array.isArray(parsed.exclude)) return false;
|
|
731
|
+
return parsed.exclude.some(
|
|
732
|
+
(e) => typeof e === "string" && (e === PI_EXCLUDE || e === "./.pi" || e === ".pi/**")
|
|
733
|
+
);
|
|
734
|
+
} catch {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function insertPiExclude(rawText) {
|
|
739
|
+
const excludeArrayRe = /("exclude"\s*:\s*\[)(\s*)/;
|
|
740
|
+
if (excludeArrayRe.test(rawText)) {
|
|
741
|
+
return rawText.replace(excludeArrayRe, (_m, open, ws) => {
|
|
742
|
+
const sep = ws.includes("\n") ? ws : " ";
|
|
743
|
+
return `${open}${sep}"${PI_EXCLUDE}",${ws.includes("\n") ? "" : " "}`;
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const includeArrayRe = /("include"\s*:\s*\[[^\]]*\])/;
|
|
747
|
+
if (includeArrayRe.test(rawText)) {
|
|
748
|
+
return rawText.replace(includeArrayRe, (m) => `${m},
|
|
749
|
+
"exclude": ["${PI_EXCLUDE}"]`);
|
|
750
|
+
}
|
|
751
|
+
const lastBrace = rawText.lastIndexOf("}");
|
|
752
|
+
if (lastBrace > 0 && rawText.slice(0, lastBrace).includes(":")) {
|
|
753
|
+
const before = rawText.slice(0, lastBrace).replace(/\s*$/, "");
|
|
754
|
+
const after = rawText.slice(lastBrace);
|
|
755
|
+
const needsComma = !before.endsWith(",") && !before.endsWith("{");
|
|
756
|
+
return `${before}${needsComma ? "," : ""}
|
|
757
|
+
"exclude": ["${PI_EXCLUDE}"]
|
|
758
|
+
${after}`;
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
function ensurePiExcludedFromTsconfig(targetDir) {
|
|
763
|
+
const path = join4(targetDir, "tsconfig.json");
|
|
764
|
+
if (!existsSync4(path)) return { status: "no-tsconfig" };
|
|
765
|
+
let rawText;
|
|
766
|
+
try {
|
|
767
|
+
rawText = readFileSync3(path, "utf-8");
|
|
768
|
+
} catch {
|
|
769
|
+
return { status: "skipped", reason: "tsconfig.json could not be read" };
|
|
770
|
+
}
|
|
771
|
+
if (alreadyExcludesPi(rawText)) {
|
|
772
|
+
return { status: "already-present", path };
|
|
773
|
+
}
|
|
774
|
+
const updated = insertPiExclude(rawText);
|
|
775
|
+
if (updated === null || updated === rawText) {
|
|
776
|
+
return {
|
|
777
|
+
status: "skipped",
|
|
778
|
+
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'
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
if (!alreadyExcludesPi(updated)) {
|
|
782
|
+
return {
|
|
783
|
+
status: "skipped",
|
|
784
|
+
reason: 'tsconfig.json edit could not be verified \u2014 add ".pi" to its "exclude" array manually'
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
writeFileSync2(path, updated, "utf-8");
|
|
789
|
+
} catch {
|
|
790
|
+
return { status: "skipped", reason: "tsconfig.json could not be written" };
|
|
791
|
+
}
|
|
792
|
+
return { status: "added", path };
|
|
793
|
+
}
|
|
794
|
+
|
|
716
795
|
// src/init.ts
|
|
796
|
+
var BACKLOG_README = `# Backlog
|
|
797
|
+
|
|
798
|
+
Deferred work lives here \u2014 ideas and follow-ups you surface mid-sprint but
|
|
799
|
+
can't take on in the current feature. Capturing them keeps the current spec
|
|
800
|
+
focused without losing the thread.
|
|
801
|
+
|
|
802
|
+
- One file per item: \`docs/backlog/YYYY-MM-DD-<short-name>.md\`.
|
|
803
|
+
- Joycraft skills (\`/joycraft-interview\`, \`/joycraft-new-feature\`,
|
|
804
|
+
\`/joycraft-design\`) offer to write entries here \u2014 always with your
|
|
805
|
+
confirmation, never automatically.
|
|
806
|
+
- Promote an item by turning it into a Feature Brief under
|
|
807
|
+
\`docs/features/<slug>/\` when you're ready to build it.
|
|
808
|
+
`;
|
|
717
809
|
function ensureDir(dir) {
|
|
718
|
-
if (!
|
|
810
|
+
if (!existsSync5(dir)) {
|
|
719
811
|
mkdirSync2(dir, { recursive: true });
|
|
720
812
|
}
|
|
721
813
|
}
|
|
722
814
|
function writeFile(path, content, force, result) {
|
|
723
|
-
if (
|
|
815
|
+
if (existsSync5(path) && !force) {
|
|
724
816
|
result.skipped.push(path);
|
|
725
817
|
return;
|
|
726
818
|
}
|
|
727
|
-
|
|
819
|
+
writeFileSync3(path, content, "utf-8");
|
|
728
820
|
result.created.push(path);
|
|
729
821
|
}
|
|
730
822
|
async function init(dir, opts) {
|
|
731
823
|
const targetDir = resolve(dir);
|
|
732
824
|
const result = { created: [], skipped: [], modified: [], warnings: [] };
|
|
733
825
|
const stack = await detectStack(targetDir);
|
|
734
|
-
const isPi =
|
|
826
|
+
const isPi = existsSync5(join5(targetDir, ".pi"));
|
|
735
827
|
const harnesses = await resolveHarnesses(process.stdin.isTTY === true);
|
|
736
828
|
const wants = (h) => harnesses.includes(h);
|
|
737
829
|
if (harnesses.length === 0) {
|
|
@@ -746,14 +838,16 @@ async function init(dir, opts) {
|
|
|
746
838
|
interactive: process.stdin.isTTY === true,
|
|
747
839
|
promptIntro: "\nHow should Joycraft files be tracked in git?"
|
|
748
840
|
});
|
|
749
|
-
ensureDir(
|
|
750
|
-
|
|
841
|
+
ensureDir(join5(targetDir, "docs", "context"));
|
|
842
|
+
ensureDir(join5(targetDir, "docs", "backlog"));
|
|
843
|
+
writeFile(join5(targetDir, "docs", "backlog", "README.md"), BACKLOG_README, opts.force, result);
|
|
844
|
+
const skillsDir = join5(targetDir, ".claude", "skills");
|
|
751
845
|
let existingSkills = [];
|
|
752
|
-
if (wants("claude") &&
|
|
846
|
+
if (wants("claude") && existsSync5(skillsDir)) {
|
|
753
847
|
existingSkills = readdirSync2(skillsDir).filter((name) => {
|
|
754
848
|
if (name.startsWith("joycraft-")) return false;
|
|
755
849
|
if (name.startsWith(".")) return false;
|
|
756
|
-
const fullPath =
|
|
850
|
+
const fullPath = join5(skillsDir, name);
|
|
757
851
|
try {
|
|
758
852
|
return statSync(fullPath).isDirectory();
|
|
759
853
|
} catch {
|
|
@@ -764,32 +858,32 @@ async function init(dir, opts) {
|
|
|
764
858
|
if (wants("claude")) {
|
|
765
859
|
for (const [filename, content] of Object.entries(SKILLS)) {
|
|
766
860
|
const skillName = filename.replace(/\.md$/, "");
|
|
767
|
-
const skillDir =
|
|
861
|
+
const skillDir = join5(skillsDir, skillName);
|
|
768
862
|
ensureDir(skillDir);
|
|
769
|
-
writeFile(
|
|
863
|
+
writeFile(join5(skillDir, "SKILL.md"), content, opts.force, result);
|
|
770
864
|
}
|
|
771
865
|
}
|
|
772
866
|
if (wants("codex")) {
|
|
773
|
-
const codexSkillsDir =
|
|
867
|
+
const codexSkillsDir = join5(targetDir, ".agents", "skills");
|
|
774
868
|
for (const [filename, content] of Object.entries(CODEX_SKILLS)) {
|
|
775
869
|
const skillName = filename.replace(/\.md$/, "");
|
|
776
|
-
const skillDir =
|
|
870
|
+
const skillDir = join5(codexSkillsDir, skillName);
|
|
777
871
|
ensureDir(skillDir);
|
|
778
|
-
writeFile(
|
|
872
|
+
writeFile(join5(skillDir, "SKILL.md"), content, opts.force, result);
|
|
779
873
|
}
|
|
780
874
|
}
|
|
781
875
|
if (wants("pi")) {
|
|
782
|
-
const piSkillsDir =
|
|
876
|
+
const piSkillsDir = join5(targetDir, ".pi", "skills");
|
|
783
877
|
for (const [filename, content] of Object.entries(PI_SKILLS)) {
|
|
784
878
|
const skillName = filename.replace(/\.md$/, "");
|
|
785
|
-
const skillDir =
|
|
879
|
+
const skillDir = join5(piSkillsDir, skillName);
|
|
786
880
|
ensureDir(skillDir);
|
|
787
|
-
writeFile(
|
|
881
|
+
writeFile(join5(skillDir, "SKILL.md"), content, opts.force, result);
|
|
788
882
|
}
|
|
789
|
-
const piScriptsDir =
|
|
883
|
+
const piScriptsDir = join5(targetDir, ".pi", "scripts", "joycraft");
|
|
790
884
|
ensureDir(piScriptsDir);
|
|
791
885
|
for (const [name, content] of Object.entries(PI_SCRIPTS)) {
|
|
792
|
-
const scriptPath =
|
|
886
|
+
const scriptPath = join5(piScriptsDir, name);
|
|
793
887
|
writeFile(scriptPath, content, opts.force, result);
|
|
794
888
|
if (name !== "README.md") {
|
|
795
889
|
try {
|
|
@@ -798,78 +892,89 @@ async function init(dir, opts) {
|
|
|
798
892
|
}
|
|
799
893
|
}
|
|
800
894
|
}
|
|
801
|
-
const piExtDir =
|
|
895
|
+
const piExtDir = join5(targetDir, ".pi", "extensions");
|
|
802
896
|
ensureDir(piExtDir);
|
|
803
897
|
for (const [name, content] of Object.entries(PI_EXTENSIONS)) {
|
|
804
|
-
writeFile(
|
|
898
|
+
writeFile(join5(piExtDir, name), content, opts.force, result);
|
|
805
899
|
}
|
|
806
|
-
const piAgentsDir =
|
|
900
|
+
const piAgentsDir = join5(targetDir, ".pi", "agents");
|
|
807
901
|
ensureDir(piAgentsDir);
|
|
808
902
|
for (const [name, content] of Object.entries(PI_AGENTS)) {
|
|
809
|
-
writeFile(
|
|
903
|
+
writeFile(join5(piAgentsDir, name), content, opts.force, result);
|
|
810
904
|
}
|
|
811
905
|
}
|
|
812
|
-
const templatesDir =
|
|
906
|
+
const templatesDir = join5(targetDir, "docs", "templates");
|
|
813
907
|
ensureDir(templatesDir);
|
|
814
908
|
for (const [filename, content] of Object.entries(TEMPLATES)) {
|
|
815
|
-
ensureDir(dirname(
|
|
816
|
-
writeFile(
|
|
909
|
+
ensureDir(dirname(join5(templatesDir, filename)));
|
|
910
|
+
writeFile(join5(templatesDir, filename), content, opts.force, result);
|
|
817
911
|
}
|
|
818
|
-
const claudeMdPath =
|
|
819
|
-
if (
|
|
912
|
+
const claudeMdPath = join5(targetDir, "CLAUDE.md");
|
|
913
|
+
if (existsSync5(claudeMdPath) && !opts.force) {
|
|
820
914
|
result.skipped.push(claudeMdPath);
|
|
821
915
|
} else {
|
|
822
916
|
const projectName = basename(targetDir);
|
|
823
917
|
const content = generateCLAUDEMd(projectName, stack, existingSkills, {
|
|
824
918
|
privateProfile: gitignoreProfile === "private"
|
|
825
919
|
});
|
|
826
|
-
|
|
920
|
+
writeFileSync3(claudeMdPath, content, "utf-8");
|
|
827
921
|
result.created.push(claudeMdPath);
|
|
828
922
|
}
|
|
829
|
-
const agentsMdPath =
|
|
830
|
-
if (
|
|
923
|
+
const agentsMdPath = join5(targetDir, "AGENTS.md");
|
|
924
|
+
if (existsSync5(agentsMdPath) && !opts.force) {
|
|
831
925
|
result.skipped.push(agentsMdPath);
|
|
832
926
|
} else {
|
|
833
927
|
const projectName = basename(targetDir);
|
|
834
928
|
const content = generateAgentsMd(projectName, stack, gitignoreProfile === "private");
|
|
835
|
-
|
|
929
|
+
writeFileSync3(agentsMdPath, content, "utf-8");
|
|
836
930
|
result.created.push(agentsMdPath);
|
|
837
931
|
}
|
|
838
932
|
const fileHashes = {};
|
|
839
933
|
if (wants("claude")) {
|
|
840
934
|
for (const [filename, content] of Object.entries(SKILLS)) {
|
|
841
935
|
const skillName = filename.replace(/\.md$/, "");
|
|
842
|
-
fileHashes[
|
|
936
|
+
fileHashes[join5(".claude", "skills", skillName, "SKILL.md")] = hashContent(content);
|
|
843
937
|
}
|
|
844
938
|
}
|
|
845
939
|
if (wants("codex")) {
|
|
846
940
|
for (const [filename, content] of Object.entries(CODEX_SKILLS)) {
|
|
847
941
|
const skillName = filename.replace(/\.md$/, "");
|
|
848
|
-
fileHashes[
|
|
942
|
+
fileHashes[join5(".agents", "skills", skillName, "SKILL.md")] = hashContent(content);
|
|
849
943
|
}
|
|
850
944
|
}
|
|
851
945
|
for (const [filename, content] of Object.entries(TEMPLATES)) {
|
|
852
|
-
fileHashes[
|
|
946
|
+
fileHashes[join5("docs", "templates", filename)] = hashContent(content);
|
|
853
947
|
}
|
|
854
948
|
if (wants("pi")) {
|
|
855
949
|
for (const [filename, content] of Object.entries(PI_SKILLS)) {
|
|
856
950
|
const skillName = filename.replace(/\.md$/, "");
|
|
857
|
-
fileHashes[
|
|
951
|
+
fileHashes[join5(".pi", "skills", skillName, "SKILL.md")] = hashContent(content);
|
|
858
952
|
}
|
|
859
953
|
for (const [name, content] of Object.entries(PI_SCRIPTS)) {
|
|
860
|
-
fileHashes[
|
|
954
|
+
fileHashes[join5(".pi", "scripts", "joycraft", name)] = hashContent(content);
|
|
861
955
|
}
|
|
862
956
|
for (const [name, content] of Object.entries(PI_EXTENSIONS)) {
|
|
863
|
-
fileHashes[
|
|
957
|
+
fileHashes[join5(".pi", "extensions", name)] = hashContent(content);
|
|
864
958
|
}
|
|
865
959
|
for (const [name, content] of Object.entries(PI_AGENTS)) {
|
|
866
|
-
fileHashes[
|
|
960
|
+
fileHashes[join5(".pi", "agents", name)] = hashContent(content);
|
|
867
961
|
}
|
|
868
962
|
}
|
|
869
963
|
writeVersion(targetDir, getPackageVersion(), fileHashes, gitignoreProfile, harnesses);
|
|
870
964
|
applyGitignoreProfile(targetDir, gitignoreProfile);
|
|
965
|
+
if (wants("pi")) {
|
|
966
|
+
const outcome = ensurePiExcludedFromTsconfig(targetDir);
|
|
967
|
+
if (outcome.status === "added") {
|
|
968
|
+
result.modified.push(outcome.path);
|
|
969
|
+
result.warnings.push(
|
|
970
|
+
'Added ".pi" to tsconfig.json "exclude" so the Pi extension stays out of your TypeScript build.'
|
|
971
|
+
);
|
|
972
|
+
} else if (outcome.status === "skipped") {
|
|
973
|
+
result.warnings.push(outcome.reason);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
871
976
|
if (wants("claude")) {
|
|
872
|
-
const hooksDir =
|
|
977
|
+
const hooksDir = join5(targetDir, ".claude", "hooks");
|
|
873
978
|
ensureDir(hooksDir);
|
|
874
979
|
const hookScript = `// Joycraft version check \u2014 runs on Claude Code session start
|
|
875
980
|
import { readFileSync } from 'node:fs';
|
|
@@ -883,13 +988,13 @@ try {
|
|
|
883
988
|
}
|
|
884
989
|
} catch {}
|
|
885
990
|
`;
|
|
886
|
-
writeFile(
|
|
887
|
-
const settingsPath =
|
|
991
|
+
writeFile(join5(hooksDir, "joycraft-version-check.mjs"), hookScript, opts.force, result);
|
|
992
|
+
const settingsPath = join5(targetDir, ".claude", "settings.json");
|
|
888
993
|
let settings = {};
|
|
889
994
|
let settingsMalformed = false;
|
|
890
|
-
if (
|
|
995
|
+
if (existsSync5(settingsPath)) {
|
|
891
996
|
try {
|
|
892
|
-
settings = JSON.parse(
|
|
997
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
893
998
|
} catch {
|
|
894
999
|
settingsMalformed = true;
|
|
895
1000
|
result.warnings.push(
|
|
@@ -922,13 +1027,13 @@ try {
|
|
|
922
1027
|
});
|
|
923
1028
|
}
|
|
924
1029
|
if (!hasJoycraftHook || envMissing) {
|
|
925
|
-
|
|
1030
|
+
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
926
1031
|
if (!result.created.includes(settingsPath)) result.created.push(settingsPath);
|
|
927
1032
|
}
|
|
928
1033
|
const permissions = generatePermissions(stack);
|
|
929
|
-
if (
|
|
1034
|
+
if (existsSync5(settingsPath)) {
|
|
930
1035
|
try {
|
|
931
|
-
settings = JSON.parse(
|
|
1036
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
932
1037
|
} catch {
|
|
933
1038
|
result.warnings.push(
|
|
934
1039
|
"settings.json became unreadable after hook merge \u2014 skipping permissions merge.\n Fix the JSON in .claude/settings.json and re-run init."
|
|
@@ -947,7 +1052,7 @@ try {
|
|
|
947
1052
|
for (const rule of permissions.deny) {
|
|
948
1053
|
if (!perms.deny.includes(rule)) perms.deny.push(rule);
|
|
949
1054
|
}
|
|
950
|
-
|
|
1055
|
+
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
951
1056
|
}
|
|
952
1057
|
}
|
|
953
1058
|
const hookResult = installSafeguardHooks(targetDir, [], opts.force, settingsMalformed);
|
|
@@ -955,9 +1060,9 @@ try {
|
|
|
955
1060
|
result.skipped.push(...hookResult.skipped);
|
|
956
1061
|
}
|
|
957
1062
|
if (gitignoreProfile === "shared" && wants("claude")) {
|
|
958
|
-
const gitignorePath =
|
|
959
|
-
if (
|
|
960
|
-
const gitignore =
|
|
1063
|
+
const gitignorePath = join5(targetDir, ".gitignore");
|
|
1064
|
+
if (existsSync5(gitignorePath)) {
|
|
1065
|
+
const gitignore = readFileSync4(gitignorePath, "utf-8");
|
|
961
1066
|
if (/^\.claude\/?$/m.test(gitignore) || /^\.claude\/\*$/m.test(gitignore)) {
|
|
962
1067
|
result.warnings.push(
|
|
963
1068
|
".claude/ is in your .gitignore \u2014 teammates won't get Joycraft skills.\n Add this line to .gitignore to fix: !.claude/skills/"
|
|
@@ -1041,4 +1146,4 @@ function printSummary(result, stack, existingSkills = [], isPi = false, gitignor
|
|
|
1041
1146
|
export {
|
|
1042
1147
|
init
|
|
1043
1148
|
};
|
|
1044
|
-
//# sourceMappingURL=init-
|
|
1149
|
+
//# sourceMappingURL=init-OO77DURX.js.map
|