joycraft 0.6.9 → 0.6.11
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 +27 -0
- package/dist/chunk-IELXGWSH.js +21 -0
- package/dist/chunk-IELXGWSH.js.map +1 -0
- package/dist/{chunk-63OWWRAJ.js → chunk-TD65VH2W.js} +21 -4
- package/dist/chunk-TD65VH2W.js.map +1 -0
- package/dist/chunk-VIVJUY6J.js +86 -0
- package/dist/chunk-VIVJUY6J.js.map +1 -0
- package/dist/cli.js +23 -8
- package/dist/cli.js.map +1 -1
- package/dist/{init-35PB3SOQ.js → init-TR43TRW2.js} +41 -16
- package/dist/init-TR43TRW2.js.map +1 -0
- package/dist/{init-autofix-3QKXF5UX.js → init-autofix-N3P63CAT.js} +2 -2
- package/dist/{upgrade-G7PYJXEE.js → upgrade-L3HSCFTV.js} +28 -9
- package/dist/upgrade-L3HSCFTV.js.map +1 -0
- package/dist/{version-HEQ42K47.js → version-2FGZETKD.js} +6 -2
- package/package.json +1 -1
- package/dist/chunk-63OWWRAJ.js.map +0 -1
- package/dist/chunk-JVRMYMBC.js +0 -39
- package/dist/chunk-JVRMYMBC.js.map +0 -1
- package/dist/init-35PB3SOQ.js.map +0 -1
- package/dist/upgrade-G7PYJXEE.js.map +0 -1
- /package/dist/{init-autofix-3QKXF5UX.js.map → init-autofix-N3P63CAT.js.map} +0 -0
- /package/dist/{version-HEQ42K47.js.map → version-2FGZETKD.js.map} +0 -0
package/README.md
CHANGED
|
@@ -78,6 +78,33 @@ Joycraft auto-detects your tech stack and creates:
|
|
|
78
78
|
- **Context documents** in `docs/context/`: production map, dangerous assumptions, decision log, institutional knowledge, and troubleshooting guide
|
|
79
79
|
- **Templates** including atomic spec, feature brief, implementation plan, boundary framework, and workflow templates for scenario generation and autofix loops
|
|
80
80
|
|
|
81
|
+
### Git tracking: shared vs private
|
|
82
|
+
|
|
83
|
+
By default Joycraft assumes you want to **commit** the harness so your whole team
|
|
84
|
+
gets the same skills and workflow. Some teams prefer to keep the harness local
|
|
85
|
+
and track only the docs. Choose a profile at init time:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx joycraft init --gitignore=shared # default — commit .claude/, .agents/, .pi/
|
|
89
|
+
npx joycraft init --gitignore=private # gitignore them; track only CLAUDE.md, AGENTS.md, docs/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Run interactively without the flag and `init` will ask. The choice is saved, so
|
|
93
|
+
`npx joycraft upgrade` re-applies it automatically. To switch an existing
|
|
94
|
+
project later (or decide from CI), pass the same flag to upgrade:
|
|
95
|
+
`npx joycraft upgrade --gitignore=private`. `.gitignore` edits are
|
|
96
|
+
append-only — Joycraft never rewrites or removes your existing lines.
|
|
97
|
+
|
|
98
|
+
| Profile | Tracked in git | Gitignored |
|
|
99
|
+
|---------|----------------|------------|
|
|
100
|
+
| `shared` (default) | `CLAUDE.md`, `AGENTS.md`, `docs/`, `.claude/skills/`, `.agents/`, `.pi/` | hidden upgrade state only |
|
|
101
|
+
| `private` | `CLAUDE.md`, `AGENTS.md`, `docs/` | `.claude/`, `.agents/`, `.pi/` |
|
|
102
|
+
|
|
103
|
+
> Switching an existing project to `private` only updates `.gitignore`. If
|
|
104
|
+
> harness files were already committed, untrack them with
|
|
105
|
+
> `git rm -r --cached .claude .agents .pi` (Joycraft prints this reminder and
|
|
106
|
+
> never runs git for you).
|
|
107
|
+
|
|
81
108
|
### Supported Stacks
|
|
82
109
|
|
|
83
110
|
Node.js (npm/pnpm/yarn/bun), Python (poetry/pip/uv), Rust, Go, Swift, and generic (Makefile/Dockerfile).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/package-version.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
function getPackageVersion() {
|
|
9
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
10
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
11
|
+
const pkg = JSON.parse(raw);
|
|
12
|
+
if (typeof pkg.version !== "string" || pkg.version.length === 0) {
|
|
13
|
+
throw new Error(`Joycraft package.json at ${pkgPath} is missing a version field`);
|
|
14
|
+
}
|
|
15
|
+
return pkg.version;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
getPackageVersion
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=chunk-IELXGWSH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/package-version.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport function getPackageVersion(): string {\n const pkgPath = join(__dirname, '..', 'package.json');\n const raw = readFileSync(pkgPath, 'utf-8');\n const pkg = JSON.parse(raw) as { version?: unknown };\n if (typeof pkg.version !== 'string' || pkg.version.length === 0) {\n throw new Error(`Joycraft package.json at ${pkgPath} is missing a version field`);\n }\n return pkg.version;\n}\n"],"mappings":";;;AAAA,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAE9B,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AAEjD,SAAS,oBAA4B;AAC1C,QAAM,UAAU,KAAK,WAAW,MAAM,cAAc;AACpD,QAAM,MAAM,aAAa,SAAS,OAAO;AACzC,QAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,MAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,WAAW,GAAG;AAC/D,UAAM,IAAI,MAAM,4BAA4B,OAAO,6BAA6B;AAAA,EAClF;AACA,SAAO,IAAI;AACb;","names":[]}
|
|
@@ -7,6 +7,11 @@ import { createHash } from "crypto";
|
|
|
7
7
|
var STATE_PATH = join(".claude", ".joycraft", "state.json");
|
|
8
8
|
var LEGACY_VERSION_FILE = ".joycraft-version";
|
|
9
9
|
var HASH_LENGTH = 16;
|
|
10
|
+
var DEFAULT_GITIGNORE_PROFILE = "shared";
|
|
11
|
+
function parseGitignoreProfile(value) {
|
|
12
|
+
const v = typeof value === "string" ? value.trim().toLowerCase() : value;
|
|
13
|
+
return v === "shared" || v === "private" ? v : null;
|
|
14
|
+
}
|
|
10
15
|
function hashContent(content) {
|
|
11
16
|
return createHash("sha256").update(content).digest("hex");
|
|
12
17
|
}
|
|
@@ -20,20 +25,30 @@ function readVersion(dir) {
|
|
|
20
25
|
const raw = readFileSync(filePath, "utf-8");
|
|
21
26
|
const parsed = JSON.parse(raw);
|
|
22
27
|
if (typeof parsed.version === "string" && typeof parsed.files === "object") {
|
|
23
|
-
|
|
28
|
+
const profile = parseGitignoreProfile(parsed.gitignoreProfile);
|
|
29
|
+
return {
|
|
30
|
+
version: parsed.version,
|
|
31
|
+
files: parsed.files,
|
|
32
|
+
...profile ? { gitignoreProfile: profile } : {}
|
|
33
|
+
};
|
|
24
34
|
}
|
|
25
35
|
return null;
|
|
26
36
|
} catch {
|
|
27
37
|
return null;
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
|
-
function writeVersion(dir, version, files) {
|
|
40
|
+
function writeVersion(dir, version, files, gitignoreProfile) {
|
|
31
41
|
const filePath = join(dir, STATE_PATH);
|
|
42
|
+
const profile = gitignoreProfile ?? readVersion(dir)?.gitignoreProfile;
|
|
32
43
|
const truncated = {};
|
|
33
44
|
for (const [path, hash] of Object.entries(files)) {
|
|
34
45
|
truncated[path] = truncateHash(hash);
|
|
35
46
|
}
|
|
36
|
-
const data = {
|
|
47
|
+
const data = {
|
|
48
|
+
version,
|
|
49
|
+
files: truncated,
|
|
50
|
+
...profile ? { gitignoreProfile: profile } : {}
|
|
51
|
+
};
|
|
37
52
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
38
53
|
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
39
54
|
}
|
|
@@ -49,10 +64,12 @@ function getLevel(dir) {
|
|
|
49
64
|
export {
|
|
50
65
|
STATE_PATH,
|
|
51
66
|
LEGACY_VERSION_FILE,
|
|
67
|
+
DEFAULT_GITIGNORE_PROFILE,
|
|
68
|
+
parseGitignoreProfile,
|
|
52
69
|
hashContent,
|
|
53
70
|
truncateHash,
|
|
54
71
|
readVersion,
|
|
55
72
|
writeVersion,
|
|
56
73
|
getLevel
|
|
57
74
|
};
|
|
58
|
-
//# sourceMappingURL=chunk-
|
|
75
|
+
//# sourceMappingURL=chunk-TD65VH2W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { createHash } from 'node:crypto';\n\n/**\n * Project-relative path to Joycraft's upgrade-state file.\n *\n * Hidden inside `.claude/` — the dir `init` always creates for every harness —\n * directly analogous to npm's own hidden lockfile at\n * `node_modules/.package-lock.json`. Never at the repo root, never committed\n * (init/upgrade gitignore it). The old root location was `.joycraft-version`\n * (see LEGACY_VERSION_FILE); `upgrade` migrates it on first run.\n */\nexport const STATE_PATH = join('.claude', '.joycraft', 'state.json');\n\n/** The pre-relocation root path. Kept only so `upgrade` can migrate it. */\nexport const LEGACY_VERSION_FILE = '.joycraft-version';\n\n/**\n * Length we truncate stored hashes to. Full SHA-256 is 64 hex chars; 16 hex\n * (64 bits) is ample to detect customization across ~100 managed files and\n * shrinks the state ~4×. Compare truncated-on-both-sides (see upgrade.ts).\n */\nconst HASH_LENGTH = 16;\n\n/**\n * How much of the Joycraft harness is tracked in git.\n * - `shared` — commit skills/agents/pi so teammates get the workflow (default).\n * - `private` — gitignore .claude/, .agents/, .pi/; track only CLAUDE.md,\n * AGENTS.md, and docs/.\n */\nexport type GitignoreProfile = 'shared' | 'private';\n\nexport const DEFAULT_GITIGNORE_PROFILE: GitignoreProfile = 'shared';\n\n/**\n * Narrow an arbitrary value to a GitignoreProfile, or null if unrecognized.\n * Strings are normalized (trim + lowercase) here so every call site — flag,\n * prompt, persisted state — gets the same case-insensitivity for free.\n */\nexport function parseGitignoreProfile(value: unknown): GitignoreProfile | null {\n const v = typeof value === 'string' ? value.trim().toLowerCase() : value;\n return v === 'shared' || v === 'private' ? v : null;\n}\n\nexport interface VersionInfo {\n version: string;\n files: Record<string, string>;\n /**\n * The gitignore profile chosen at init/upgrade. Absent on state written by\n * Joycraft versions before this field existed — treat absent as `shared`.\n */\n gitignoreProfile?: GitignoreProfile;\n}\n\nexport function hashContent(content: string): string {\n return createHash('sha256').update(content).digest('hex');\n}\n\n/** Truncate a (full) content hash to the stored length. Idempotent for already-short input. */\nexport function truncateHash(hash: string): string {\n return hash.slice(0, HASH_LENGTH);\n}\n\nexport function readVersion(dir: string): VersionInfo | null {\n const filePath = join(dir, STATE_PATH);\n if (!existsSync(filePath)) return null;\n try {\n const raw = readFileSync(filePath, 'utf-8');\n const parsed = JSON.parse(raw);\n if (typeof parsed.version === 'string' && typeof parsed.files === 'object') {\n // Sanitize the profile: ignore unknown/legacy values rather than\n // returning them (absent or bogus → undefined, callers default to shared).\n const profile = parseGitignoreProfile(parsed.gitignoreProfile);\n return {\n version: parsed.version,\n files: parsed.files,\n ...(profile ? { gitignoreProfile: profile } : {}),\n };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport function writeVersion(\n dir: string,\n version: string,\n files: Record<string, string>,\n gitignoreProfile?: GitignoreProfile\n): void {\n const filePath = join(dir, STATE_PATH);\n // An omitted profile means \"no new decision\", not \"clear it\": preserve\n // whatever is already persisted so call sites that only refresh\n // version/hashes can never silently strip a saved choice.\n const profile = gitignoreProfile ?? readVersion(dir)?.gitignoreProfile;\n // Store truncated hashes — single source of truth for the on-disk shape.\n const truncated: Record<string, string> = {};\n for (const [path, hash] of Object.entries(files)) {\n truncated[path] = truncateHash(hash);\n }\n const data: VersionInfo = {\n version,\n files: truncated,\n ...(profile ? { gitignoreProfile: profile } : {}),\n };\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, JSON.stringify(data, null, 2) + '\\n', 'utf-8');\n}\n\n/**\n * Detect the current Joycraft harness level for a project directory.\n * Returns 5 if Level 5 artifacts (autofix workflow + External Validation) are present, 4 otherwise.\n */\nexport function getLevel(dir: string): number {\n const hasAutofix = existsSync(join(dir, '.github', 'workflows', 'autofix.yml'));\n if (!hasAutofix) return 4;\n const claudeMdPath = join(dir, 'CLAUDE.md');\n if (!existsSync(claudeMdPath)) return 4;\n const content = readFileSync(claudeMdPath, 'utf-8');\n return content.includes('## External Validation') ? 5 : 4;\n}\n"],"mappings":";;;AAAA,SAAS,cAAc,eAAe,YAAY,iBAAiB;AACnE,SAAS,MAAM,eAAe;AAC9B,SAAS,kBAAkB;AAWpB,IAAM,aAAa,KAAK,WAAW,aAAa,YAAY;AAG5D,IAAM,sBAAsB;AAOnC,IAAM,cAAc;AAUb,IAAM,4BAA8C;AAOpD,SAAS,sBAAsB,OAAyC;AAC7E,QAAM,IAAI,OAAO,UAAU,WAAW,MAAM,KAAK,EAAE,YAAY,IAAI;AACnE,SAAO,MAAM,YAAY,MAAM,YAAY,IAAI;AACjD;AAYO,SAAS,YAAY,SAAyB;AACnD,SAAO,WAAW,QAAQ,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAC1D;AAGO,SAAS,aAAa,MAAsB;AACjD,SAAO,KAAK,MAAM,GAAG,WAAW;AAClC;AAEO,SAAS,YAAY,KAAiC;AAC3D,QAAM,WAAW,KAAK,KAAK,UAAU;AACrC,MAAI,CAAC,WAAW,QAAQ,EAAG,QAAO;AAClC,MAAI;AACF,UAAM,MAAM,aAAa,UAAU,OAAO;AAC1C,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,OAAO,OAAO,YAAY,YAAY,OAAO,OAAO,UAAU,UAAU;AAG1E,YAAM,UAAU,sBAAsB,OAAO,gBAAgB;AAC7D,aAAO;AAAA,QACL,SAAS,OAAO;AAAA,QAChB,OAAO,OAAO;AAAA,QACd,GAAI,UAAU,EAAE,kBAAkB,QAAQ,IAAI,CAAC;AAAA,MACjD;AAAA,IACF;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,aACd,KACA,SACA,OACA,kBACM;AACN,QAAM,WAAW,KAAK,KAAK,UAAU;AAIrC,QAAM,UAAU,oBAAoB,YAAY,GAAG,GAAG;AAEtD,QAAM,YAAoC,CAAC;AAC3C,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,cAAU,IAAI,IAAI,aAAa,IAAI;AAAA,EACrC;AACA,QAAM,OAAoB;AAAA,IACxB;AAAA,IACA,OAAO;AAAA,IACP,GAAI,UAAU,EAAE,kBAAkB,QAAQ,IAAI,CAAC;AAAA,EACjD;AACA,YAAU,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAChD,gBAAc,UAAU,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI,MAAM,OAAO;AACvE;AAMO,SAAS,SAAS,KAAqB;AAC5C,QAAM,aAAa,WAAW,KAAK,KAAK,WAAW,aAAa,aAAa,CAAC;AAC9E,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,eAAe,KAAK,KAAK,WAAW;AAC1C,MAAI,CAAC,WAAW,YAAY,EAAG,QAAO;AACtC,QAAM,UAAU,aAAa,cAAc,OAAO;AAClD,SAAO,QAAQ,SAAS,wBAAwB,IAAI,IAAI;AAC1D;","names":[]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_GITIGNORE_PROFILE,
|
|
4
|
+
STATE_PATH,
|
|
5
|
+
parseGitignoreProfile
|
|
6
|
+
} from "./chunk-TD65VH2W.js";
|
|
7
|
+
|
|
8
|
+
// src/gitignore.ts
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { createInterface } from "readline";
|
|
12
|
+
var PRIVATE_PROFILE_IGNORES = [".claude/", ".agents/", ".pi/"];
|
|
13
|
+
var PRIVATE_DIRS_DISPLAY = PRIVATE_PROFILE_IGNORES.join(", ");
|
|
14
|
+
var PRIVATE_UNTRACK_COMMAND = `git rm -r --cached ${PRIVATE_PROFILE_IGNORES.map((d) => d.replace(/\/$/, "")).join(" ")}`;
|
|
15
|
+
function ensureGitignoreEntries(targetDir, entries) {
|
|
16
|
+
const gitignorePath = join(targetDir, ".gitignore");
|
|
17
|
+
const current = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
|
|
18
|
+
const present = new Set(current.split("\n").map((l) => l.trim()));
|
|
19
|
+
const missing = entries.filter((e) => !present.has(e.trim()));
|
|
20
|
+
if (missing.length === 0) return [];
|
|
21
|
+
const sep = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
|
|
22
|
+
writeFileSync(gitignorePath, current + sep + missing.join("\n") + "\n", "utf-8");
|
|
23
|
+
return missing;
|
|
24
|
+
}
|
|
25
|
+
function applyGitignoreProfile(targetDir, profile) {
|
|
26
|
+
if (profile === "private") {
|
|
27
|
+
return ensureGitignoreEntries(targetDir, PRIVATE_PROFILE_IGNORES);
|
|
28
|
+
}
|
|
29
|
+
return ensureGitignoreEntries(targetDir, [STATE_PATH]);
|
|
30
|
+
}
|
|
31
|
+
function validateGitignoreFlag(flag) {
|
|
32
|
+
const parsed = parseGitignoreProfile(flag);
|
|
33
|
+
if (!parsed) {
|
|
34
|
+
throw new Error(`Unknown gitignore profile '${flag}'. Use 'shared' or 'private'.`);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
async function promptGitignoreProfile(intro) {
|
|
39
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
40
|
+
console.log(intro);
|
|
41
|
+
console.log(" shared \u2014 commit skills so your team gets the same workflow (default)");
|
|
42
|
+
console.log(` private \u2014 gitignore ${PRIVATE_DIRS_DISPLAY}; track only CLAUDE.md, AGENTS.md, docs/`);
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const ask = () => {
|
|
45
|
+
rl.question(`Profile [shared/private] (${DEFAULT_GITIGNORE_PROFILE}): `, (answer) => {
|
|
46
|
+
if (answer.trim() === "") {
|
|
47
|
+
rl.close();
|
|
48
|
+
resolve(DEFAULT_GITIGNORE_PROFILE);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const parsed = parseGitignoreProfile(answer);
|
|
52
|
+
if (parsed) {
|
|
53
|
+
rl.close();
|
|
54
|
+
resolve(parsed);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.log(
|
|
58
|
+
`Unrecognized answer '${answer.trim()}' \u2014 type 'shared' or 'private', or press Enter for ${DEFAULT_GITIGNORE_PROFILE}.`
|
|
59
|
+
);
|
|
60
|
+
ask();
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
ask();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function resolveGitignoreProfile(opts) {
|
|
67
|
+
if (opts.flag !== void 0) {
|
|
68
|
+
return { profile: validateGitignoreFlag(opts.flag), decided: true };
|
|
69
|
+
}
|
|
70
|
+
if (opts.persisted) {
|
|
71
|
+
return { profile: opts.persisted, decided: true };
|
|
72
|
+
}
|
|
73
|
+
if (opts.interactive) {
|
|
74
|
+
return { profile: await promptGitignoreProfile(opts.promptIntro), decided: true };
|
|
75
|
+
}
|
|
76
|
+
return { profile: DEFAULT_GITIGNORE_PROFILE, decided: false };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
PRIVATE_DIRS_DISPLAY,
|
|
81
|
+
PRIVATE_UNTRACK_COMMAND,
|
|
82
|
+
applyGitignoreProfile,
|
|
83
|
+
validateGitignoreFlag,
|
|
84
|
+
resolveGitignoreProfile
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=chunk-VIVJUY6J.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 * - `shared` — ignore only the hidden upgrade-state file (current default).\n * - `private` — ignore the whole .claude/, .agents/, .pi/ trees. Since\n * .claude/ already covers the state file, the writer skips the redundant\n * state entry to avoid a dead line.\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);\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;AAmBO,SAAS,sBAAsB,WAAmB,SAAqC;AAC5F,MAAI,YAAY,WAAW;AACzB,WAAO,uBAAuB,WAAW,uBAAuB;AAAA,EAClE;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,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PRIVATE_DIRS_DISPLAY
|
|
4
|
+
} from "./chunk-VIVJUY6J.js";
|
|
5
|
+
import "./chunk-TD65VH2W.js";
|
|
2
6
|
|
|
3
7
|
// src/cli.ts
|
|
4
8
|
import { Command } from "commander";
|
|
@@ -7,25 +11,36 @@ import { fileURLToPath } from "url";
|
|
|
7
11
|
import { dirname, join } from "path";
|
|
8
12
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
13
|
var pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
14
|
+
var GITIGNORE_OPTION_DESC = `Gitignore profile: 'shared' (commit skills) or 'private' (gitignore ${PRIVATE_DIRS_DISPLAY})`;
|
|
10
15
|
var program = new Command();
|
|
11
16
|
program.name("joycraft").description("Scaffold and upgrade AI development harnesses").version(pkg.version, "-v, --version");
|
|
12
|
-
program.command("init").description("Scaffold the Joycraft harness into the current project").argument("[dir]", "Target directory", ".").option("--force", "Overwrite existing files").action(async (dir, opts) => {
|
|
13
|
-
const { init } = await import("./init-
|
|
14
|
-
|
|
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-TR43TRW2.js");
|
|
19
|
+
try {
|
|
20
|
+
await init(dir, { force: opts.force ?? false, gitignore: opts.gitignore });
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
15
25
|
});
|
|
16
|
-
program.command("upgrade").description("Upgrade installed Joycraft templates and skills to latest").argument("[dir]", "Target directory", ".").option("--yes", "Auto-accept all updates").action(async (dir, opts) => {
|
|
17
|
-
const { upgrade } = await import("./upgrade-
|
|
18
|
-
|
|
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-L3HSCFTV.js");
|
|
28
|
+
try {
|
|
29
|
+
await upgrade(dir, { yes: opts.yes ?? false, gitignore: opts.gitignore });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
19
34
|
});
|
|
20
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) => {
|
|
21
|
-
const { initAutofix } = await import("./init-autofix-
|
|
36
|
+
const { initAutofix } = await import("./init-autofix-N3P63CAT.js");
|
|
22
37
|
await initAutofix(dir, opts);
|
|
23
38
|
});
|
|
24
39
|
program.command("check-version").description("Check if a newer version of Joycraft is available").action(async () => {
|
|
25
40
|
try {
|
|
26
41
|
const { readFileSync: readFileSync2, existsSync } = await import("fs");
|
|
27
42
|
const { join: join2 } = await import("path");
|
|
28
|
-
const { STATE_PATH, LEGACY_VERSION_FILE } = await import("./version-
|
|
43
|
+
const { STATE_PATH, LEGACY_VERSION_FILE } = await import("./version-2FGZETKD.js");
|
|
29
44
|
const statePath = existsSync(join2(process.cwd(), STATE_PATH)) ? join2(process.cwd(), STATE_PATH) : join2(process.cwd(), LEGACY_VERSION_FILE);
|
|
30
45
|
const data = JSON.parse(readFileSync2(statePath, "utf-8"));
|
|
31
46
|
const res = await fetch("https://registry.npmjs.org/joycraft/latest", { signal: AbortSignal.timeout(3e3) });
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { readFileSync } from 'fs';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));\n\nconst program = new Command();\n\nprogram\n .name('joycraft')\n .description('Scaffold and upgrade AI development harnesses')\n .version(pkg.version, '-v, --version');\n\nprogram\n .command('init')\n .description('Scaffold the Joycraft harness into the current project')\n .argument('[dir]', 'Target directory', '.')\n .option('--force', 'Overwrite existing files')\n .action(async (dir: string, opts: { force?: boolean }) => {\n const { init } = await import('./init.js');\n await init(dir, { force: opts.force ?? false });\n });\n\nprogram\n .command('upgrade')\n .description('Upgrade installed Joycraft templates and skills to latest')\n .argument('[dir]', 'Target directory', '.')\n .option('--yes', 'Auto-accept all updates')\n .action(async (dir: string, opts: { yes?: boolean }) => {\n const { upgrade } = await import('./upgrade.js');\n await upgrade(dir, { yes: opts.yes ?? false });\n });\n\nprogram\n .command('init-autofix')\n .description('Set up the Level 5 auto-fix loop with holdout scenarios')\n .argument('[dir]', 'Target directory', '.')\n .option('--scenarios-repo <name>', 'Name for scenarios repo')\n .option('--app-id <id>', 'GitHub App ID for Joycraft Autofix')\n .option('--force', 'Overwrite existing workflow files')\n .option('--dry-run', 'Show what would be created without creating it')\n .action(async (dir: string, opts: { scenariosRepo?: string; appId?: string; force?: boolean; dryRun?: boolean }) => {\n const { initAutofix } = await import('./init-autofix.js');\n await initAutofix(dir, opts);\n });\n\nprogram\n .command('check-version')\n .description('Check if a newer version of Joycraft is available')\n .action(async () => {\n try {\n const { readFileSync, existsSync } = await import('node:fs');\n const { join } = await import('node:path');\n const { STATE_PATH, LEGACY_VERSION_FILE } = await import('./version.js');\n // Prefer the hidden state; fall back to the legacy root file for projects\n // that have not been upgraded (which relocates it) yet.\n const statePath = existsSync(join(process.cwd(), STATE_PATH))\n ? join(process.cwd(), STATE_PATH)\n : join(process.cwd(), LEGACY_VERSION_FILE);\n const data = JSON.parse(readFileSync(statePath, 'utf-8'));\n const res = await fetch('https://registry.npmjs.org/joycraft/latest', { signal: AbortSignal.timeout(3000) });\n if (res.ok) {\n const latest = ((await res.json()) as { version: string }).version;\n if (data.version !== latest) {\n console.log(`Joycraft ${latest} available (you have ${data.version}). Run: npm install -g joycraft`);\n }\n }\n } catch {\n // Silent — don't block session start\n }\n });\n\n// Start update check immediately so it runs in parallel with the command\nconst updateCheckPromise = (async (): Promise<string | null> => {\n try {\n const res = await fetch('https://registry.npmjs.org/joycraft/latest', {\n signal: AbortSignal.timeout(3000)\n });\n if (res.ok) {\n const latest = ((await res.json()) as { version: string }).version;\n if (latest !== pkg.version) {\n return `\\nJoycraft ${latest} available (you have ${pkg.version}). Run: npm install -g joycraft`;\n }\n }\n } catch {\n // Silent — don't block or error on network issues\n }\n return null;\n})();\n\n// Print update nudge after every command\nprogram.hook('postAction', async () => {\n const message = await updateCheckPromise;\n if (message) {\n console.log(message);\n }\n});\n\n// Show help when no arguments provided\nif (process.argv.length <= 2) {\n program.outputHelp();\n process.exit(0);\n}\n\nprogram.parse();\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { readFileSync } from 'fs';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\nimport { PRIVATE_DIRS_DISPLAY } from './gitignore.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));\n\nconst GITIGNORE_OPTION_DESC = `Gitignore profile: 'shared' (commit skills) or 'private' (gitignore ${PRIVATE_DIRS_DISPLAY})`;\n\nconst program = new Command();\n\nprogram\n .name('joycraft')\n .description('Scaffold and upgrade AI development harnesses')\n .version(pkg.version, '-v, --version');\n\nprogram\n .command('init')\n .description('Scaffold the Joycraft harness into the current project')\n .argument('[dir]', 'Target directory', '.')\n .option('--force', 'Overwrite existing files')\n .option('--gitignore <profile>', GITIGNORE_OPTION_DESC)\n .action(async (dir: string, opts: { force?: boolean; gitignore?: string }) => {\n const { init } = await import('./init.js');\n try {\n await init(dir, { force: opts.force ?? false, gitignore: opts.gitignore });\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\nprogram\n .command('upgrade')\n .description('Upgrade installed Joycraft templates and skills to latest')\n .argument('[dir]', 'Target directory', '.')\n .option('--yes', 'Auto-accept all updates')\n .option('--gitignore <profile>', GITIGNORE_OPTION_DESC)\n .action(async (dir: string, opts: { yes?: boolean; gitignore?: string }) => {\n const { upgrade } = await import('./upgrade.js');\n try {\n await upgrade(dir, { yes: opts.yes ?? false, gitignore: opts.gitignore });\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\nprogram\n .command('init-autofix')\n .description('Set up the Level 5 auto-fix loop with holdout scenarios')\n .argument('[dir]', 'Target directory', '.')\n .option('--scenarios-repo <name>', 'Name for scenarios repo')\n .option('--app-id <id>', 'GitHub App ID for Joycraft Autofix')\n .option('--force', 'Overwrite existing workflow files')\n .option('--dry-run', 'Show what would be created without creating it')\n .action(async (dir: string, opts: { scenariosRepo?: string; appId?: string; force?: boolean; dryRun?: boolean }) => {\n const { initAutofix } = await import('./init-autofix.js');\n await initAutofix(dir, opts);\n });\n\nprogram\n .command('check-version')\n .description('Check if a newer version of Joycraft is available')\n .action(async () => {\n try {\n const { readFileSync, existsSync } = await import('node:fs');\n const { join } = await import('node:path');\n const { STATE_PATH, LEGACY_VERSION_FILE } = await import('./version.js');\n // Prefer the hidden state; fall back to the legacy root file for projects\n // that have not been upgraded (which relocates it) yet.\n const statePath = existsSync(join(process.cwd(), STATE_PATH))\n ? join(process.cwd(), STATE_PATH)\n : join(process.cwd(), LEGACY_VERSION_FILE);\n const data = JSON.parse(readFileSync(statePath, 'utf-8'));\n const res = await fetch('https://registry.npmjs.org/joycraft/latest', { signal: AbortSignal.timeout(3000) });\n if (res.ok) {\n const latest = ((await res.json()) as { version: string }).version;\n if (data.version !== latest) {\n console.log(`Joycraft ${latest} available (you have ${data.version}). Run: npm install -g joycraft`);\n }\n }\n } catch {\n // Silent — don't block session start\n }\n });\n\n// Start update check immediately so it runs in parallel with the command\nconst updateCheckPromise = (async (): Promise<string | null> => {\n try {\n const res = await fetch('https://registry.npmjs.org/joycraft/latest', {\n signal: AbortSignal.timeout(3000)\n });\n if (res.ok) {\n const latest = ((await res.json()) as { version: string }).version;\n if (latest !== pkg.version) {\n return `\\nJoycraft ${latest} available (you have ${pkg.version}). Run: npm install -g joycraft`;\n }\n }\n } catch {\n // Silent — don't block or error on network issues\n }\n return null;\n})();\n\n// Print update nudge after every command\nprogram.hook('postAction', async () => {\n const message = await updateCheckPromise;\n if (message) {\n console.log(message);\n }\n});\n\n// Show help when no arguments provided\nif (process.argv.length <= 2) {\n program.outputHelp();\n process.exit(0);\n}\n\nprogram.parse();\n"],"mappings":"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","names":["readFileSync","join"]}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
ensureGitignoreEntry,
|
|
4
3
|
getPackageVersion
|
|
5
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IELXGWSH.js";
|
|
5
|
+
import {
|
|
6
|
+
PRIVATE_DIRS_DISPLAY,
|
|
7
|
+
PRIVATE_UNTRACK_COMMAND,
|
|
8
|
+
applyGitignoreProfile,
|
|
9
|
+
resolveGitignoreProfile
|
|
10
|
+
} from "./chunk-VIVJUY6J.js";
|
|
6
11
|
import {
|
|
7
12
|
CODEX_SKILLS,
|
|
8
13
|
PI_AGENTS,
|
|
@@ -13,10 +18,12 @@ import {
|
|
|
13
18
|
TEMPLATES
|
|
14
19
|
} from "./chunk-W6AHAE7X.js";
|
|
15
20
|
import {
|
|
21
|
+
DEFAULT_GITIGNORE_PROFILE,
|
|
16
22
|
STATE_PATH,
|
|
17
23
|
hashContent,
|
|
24
|
+
readVersion,
|
|
18
25
|
writeVersion
|
|
19
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-TD65VH2W.js";
|
|
20
27
|
|
|
21
28
|
// src/init.ts
|
|
22
29
|
import { mkdirSync as mkdirSync2, existsSync as existsSync4, writeFileSync as writeFileSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync, chmodSync } from "fs";
|
|
@@ -714,6 +721,12 @@ async function init(dir, opts) {
|
|
|
714
721
|
const result = { created: [], skipped: [], modified: [], warnings: [] };
|
|
715
722
|
const stack = await detectStack(targetDir);
|
|
716
723
|
const isPi = existsSync4(join4(targetDir, ".pi"));
|
|
724
|
+
const { profile: gitignoreProfile } = await resolveGitignoreProfile({
|
|
725
|
+
flag: opts.gitignore,
|
|
726
|
+
persisted: readVersion(targetDir)?.gitignoreProfile,
|
|
727
|
+
interactive: process.stdin.isTTY === true,
|
|
728
|
+
promptIntro: "\nHow should Joycraft files be tracked in git?"
|
|
729
|
+
});
|
|
717
730
|
ensureDir(join4(targetDir, "docs", "context"));
|
|
718
731
|
const skillsDir = join4(targetDir, ".claude", "skills");
|
|
719
732
|
let existingSkills = [];
|
|
@@ -833,8 +846,8 @@ async function init(dir, opts) {
|
|
|
833
846
|
for (const [name, content] of Object.entries(PI_AGENTS)) {
|
|
834
847
|
fileHashes[join4(".pi", "agents", name)] = hashContent(content);
|
|
835
848
|
}
|
|
836
|
-
writeVersion(targetDir, getPackageVersion(), fileHashes);
|
|
837
|
-
|
|
849
|
+
writeVersion(targetDir, getPackageVersion(), fileHashes, gitignoreProfile);
|
|
850
|
+
applyGitignoreProfile(targetDir, gitignoreProfile);
|
|
838
851
|
const hooksDir = join4(targetDir, ".claude", "hooks");
|
|
839
852
|
ensureDir(hooksDir);
|
|
840
853
|
const hookScript = `// Joycraft version check \u2014 runs on Claude Code session start
|
|
@@ -911,18 +924,20 @@ try {
|
|
|
911
924
|
const hookResult = installSafeguardHooks(targetDir, [], opts.force, settingsMalformed);
|
|
912
925
|
result.created.push(...hookResult.created);
|
|
913
926
|
result.skipped.push(...hookResult.skipped);
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
927
|
+
if (gitignoreProfile === "shared") {
|
|
928
|
+
const gitignorePath = join4(targetDir, ".gitignore");
|
|
929
|
+
if (existsSync4(gitignorePath)) {
|
|
930
|
+
const gitignore = readFileSync3(gitignorePath, "utf-8");
|
|
931
|
+
if (/^\.claude\/?$/m.test(gitignore) || /^\.claude\/\*$/m.test(gitignore)) {
|
|
932
|
+
result.warnings.push(
|
|
933
|
+
".claude/ is in your .gitignore \u2014 teammates won't get Joycraft skills.\n Add this line to .gitignore to fix: !.claude/skills/"
|
|
934
|
+
);
|
|
935
|
+
}
|
|
921
936
|
}
|
|
922
937
|
}
|
|
923
|
-
printSummary(result, stack, existingSkills, isPi);
|
|
938
|
+
printSummary(result, stack, existingSkills, isPi, gitignoreProfile);
|
|
924
939
|
}
|
|
925
|
-
function printSummary(result, stack, existingSkills = [], isPi = false) {
|
|
940
|
+
function printSummary(result, stack, existingSkills = [], isPi = false, gitignoreProfile = DEFAULT_GITIGNORE_PROFILE) {
|
|
926
941
|
console.log("\nJoycraft initialized!\n");
|
|
927
942
|
if (stack.language !== "unknown") {
|
|
928
943
|
const fw = stack.framework ? ` + ${stack.framework}` : "";
|
|
@@ -933,6 +948,11 @@ function printSummary(result, stack, existingSkills = [], isPi = false) {
|
|
|
933
948
|
if (isPi) {
|
|
934
949
|
console.log(" Detected agent: Pi");
|
|
935
950
|
}
|
|
951
|
+
if (gitignoreProfile === "private") {
|
|
952
|
+
console.log(` Gitignore profile: private (${PRIVATE_DIRS_DISPLAY} are gitignored \u2014 only CLAUDE.md, AGENTS.md, docs/ are tracked)`);
|
|
953
|
+
} else {
|
|
954
|
+
console.log(" Gitignore profile: shared (skills and docs are tracked for your team)");
|
|
955
|
+
}
|
|
936
956
|
if (result.created.length > 0) {
|
|
937
957
|
console.log(`
|
|
938
958
|
Created ${result.created.length} file(s):`);
|
|
@@ -974,7 +994,12 @@ function printSummary(result, stack, existingSkills = [], isPi = false) {
|
|
|
974
994
|
}
|
|
975
995
|
console.log(" 2. Try /joycraft-new-feature to start building with the spec-driven workflow");
|
|
976
996
|
console.log(" (feature artifacts are written to docs/features/<slug>/ as you go)");
|
|
977
|
-
|
|
997
|
+
if (gitignoreProfile === "private") {
|
|
998
|
+
console.log(` 3. Commit CLAUDE.md, AGENTS.md, and docs/ \u2014 ${PRIVATE_DIRS_DISPLAY} stay local (gitignored)`);
|
|
999
|
+
console.log(` (if any harness files were already committed, run: ${PRIVATE_UNTRACK_COMMAND})`);
|
|
1000
|
+
} else {
|
|
1001
|
+
console.log(" 3. Commit .claude/skills/ and docs/ so your team gets the same workflow");
|
|
1002
|
+
}
|
|
978
1003
|
if (!isPi) {
|
|
979
1004
|
console.log(" Pi: Skills installed to .pi/skills/. Use /skill:joycraft-* to invoke.");
|
|
980
1005
|
}
|
|
@@ -983,4 +1008,4 @@ function printSummary(result, stack, existingSkills = [], isPi = false) {
|
|
|
983
1008
|
export {
|
|
984
1009
|
init
|
|
985
1010
|
};
|
|
986
|
-
//# sourceMappingURL=init-
|
|
1011
|
+
//# sourceMappingURL=init-TR43TRW2.js.map
|