@wildwinter/toolkit 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vcs.js ADDED
@@ -0,0 +1,214 @@
1
+ import { spawnSync } from 'child_process';
2
+
3
+ // src/vcs.ts
4
+ var VCS_KINDS = ["git", "perforce", "plastic", "svn"];
5
+ var isVcsKind = (value) => typeof value === "string" && VCS_KINDS.includes(value);
6
+ var VCS_LABELS = {
7
+ git: "Git",
8
+ perforce: "Perforce",
9
+ plastic: "Plastic SCM",
10
+ svn: "Subversion (SVN)"
11
+ };
12
+ var VCS_SETUP_FILE = "vcs-setup.md";
13
+ function ignoreFileFor(vcs) {
14
+ switch (vcs) {
15
+ case "git":
16
+ return ".gitignore";
17
+ case "perforce":
18
+ return ".p4ignore";
19
+ case "plastic":
20
+ return "ignore.conf";
21
+ default:
22
+ return void 0;
23
+ }
24
+ }
25
+ function vcsConfigFiles(format, vcs, bundle = "commit") {
26
+ const files = [{ path: VCS_SETUP_FILE, content: vcsSetup(format, vcs, bundle) }];
27
+ if (vcs === "git") files.push({ path: ".gitattributes", content: gitAttributes(format) });
28
+ const ignore = ignoreFileFor(vcs);
29
+ if (ignore !== void 0) files.push({ path: ignore, content: ignoreContent(format, bundle) });
30
+ return files;
31
+ }
32
+ function bundlePostureOf(format, vcs, read) {
33
+ const ignore = ignoreFileFor(vcs);
34
+ if (ignore !== void 0 && (read(ignore) ?? "").split(/\r?\n/).some((l) => l.trim() === `*.${format.bundle}`)) return "ignore";
35
+ return (read(VCS_SETUP_FILE) ?? "").includes(IGNORES_MARK) ? "ignore" : "commit";
36
+ }
37
+ var IGNORES_MARK = "This project IGNORES the compiled";
38
+ var column = (patterns) => Math.max(...patterns.map((p) => p.length)) + 1;
39
+ function gitAttributes(f) {
40
+ const source = f.source.map((ext) => `*.${ext}`);
41
+ const manifest = f.manifest !== void 0 ? `${f.manifest.dir}/*${f.manifest.suffix}` : void 0;
42
+ const w = column([...source, `*.${f.bundle}`, `*.${f.pack}`]);
43
+ const row = (pattern, attrs) => `${pattern.padEnd(w)}${pattern.length >= w ? " " : ""}${attrs}`;
44
+ const blocks = [
45
+ `# ${f.product} source is UTF-8 + LF text (pinned; never let autocrlf touch it).
46
+ ` + source.map((p) => row(p, "text eol=lf")).join("\n"),
47
+ `# Id-keyed structured merge for ${f.product} source (the '${f.tool} merge' driver).
48
+ # Register it once per clone (see ${VCS_SETUP_FILE}); until then git falls back to a
49
+ # normal text merge for these: no harm, just not structure-aware.
50
+ ` + source.map((p) => row(p, `merge=${f.driver}`)).join("\n")
51
+ ];
52
+ if (manifest !== void 0) {
53
+ blocks.push(`# An extension's manifest is the extension's own JSON, kept in the project so
54
+ # every machine reads the same one, and never edited here: text, merged as text.
55
+ ` + row(manifest, "text eol=lf"));
56
+ }
57
+ blocks.push(`# Generated artifacts. The compiled bundle is committed but REGENERATED, never
58
+ # hand-merged: keep ours on conflict and rebuild ('${f.tool} validate' catches a
59
+ # stale one). Needs a one-time 'git config merge.ours.driver true' (see ${VCS_SETUP_FILE}).
60
+ ` + row(`*.${f.bundle}`, "text eol=lf merge=ours") + `
61
+ # The packed document is a binary zip envelope, kept out of version control (.gitignore).
62
+ ` + row(`*.${f.pack}`, "binary"));
63
+ for (const b of f.binary ?? []) blocks.push(`${b.note.split("\n").map((l) => `# ${l}`).join("\n")}
64
+ ${row(b.pattern, "binary")}`);
65
+ return blocks.join("\n\n") + "\n";
66
+ }
67
+ function ignoreContent(f, bundle) {
68
+ const lines = [
69
+ `# ${f.product} generated artifacts: not source (see ${VCS_SETUP_FILE}).`,
70
+ "# Packed document (the send-and-return envelope):",
71
+ `*.${f.pack}`,
72
+ "# Unresolved merge sidecar (a lingering one is a validate error). The merge also names its clashes",
73
+ '# in the shard itself, under "unresolved", which IS committed with the file, so CI refuses an',
74
+ "# unresolved merge wherever it goes:",
75
+ `*.${f.conflict}`
76
+ ];
77
+ if (bundle === "ignore") lines.push("# Compiled bundle (built in CI, not committed under this posture):", `*.${f.bundle}`);
78
+ return lines.join("\n") + "\n";
79
+ }
80
+ function vcsSetup(f, vcs, bundle) {
81
+ const text = [...f.source, ...f.manifest !== void 0 ? [f.manifest.suffix.replace(/^\./, "")] : [], f.bundle];
82
+ const sections = {
83
+ git: `## git
84
+
85
+ \`.gitattributes\` (already emitted) pins ${f.product} source shards to UTF-8 + LF
86
+ text and marks the generated artifacts (the \`.${f.bundle}\` bundle \`merge=ours\`, the
87
+ \`.${f.pack}\` document \`binary\`). \`.gitignore\` (already emitted) keeps the document
88
+ (and the bundle, under the "ignore" posture) out of source control.
89
+
90
+ Recommended pre-commit hook (\`.git/hooks/pre-commit\`, executable):
91
+
92
+ #!/bin/sh
93
+ ${f.tool} validate || exit 1
94
+
95
+ The \`merge=${f.driver}\` rules in \`.gitattributes\` are already active; register the
96
+ drivers once per clone (git config is not repo-tracked). Until then git falls
97
+ back to a normal text merge for those files:
98
+
99
+ git config merge.${f.driver}.name "${f.product} structured merge"
100
+ git config merge.${f.driver}.driver "${f.tool} merge %O %A %B -o %A --path %P"
101
+ git config merge.ours.driver true
102
+
103
+ git calls the per-path driver directly (no \`mergetool\` wrapper needed). \`%O %A %B\`
104
+ are base, ours and theirs; the merged result is written back to \`%A\`, and \`%P\` is
105
+ the file's real path. On a conflict \`${f.tool} merge\` exits non-zero and writes a
106
+ \`.${f.conflict}\` sidecar beside that file, so the merge stays unresolved.
107
+ `,
108
+ perforce: `## Perforce
109
+
110
+ Add ${f.product}'s extensions to the typemap (\`p4 typemap\`): source shards and
111
+ the compiled bundle are TEXT with LF; the packed document is BINARY:
112
+
113
+ ${text.map((ext) => ` text //....${ext}`).join("\n")}
114
+ binary //....${f.pack}
115
+
116
+ On a unicode-mode server, set \`P4CHARSET=utf8\`. For a lock-based workflow (one
117
+ lock per file), add \`+l\` to make checkouts exclusive. A \`.p4ignore\` (already
118
+ emitted) keeps the packed document out of the depot.
119
+
120
+ Perforce allows ONE global merge tool, so set \`${f.tool} mergetool\` as it: it
121
+ runs the structured merge for ${f.product} source and hands everything else to
122
+ your normal tool. Map Perforce's variables into the order BASE THEIRS OURS OUT:
123
+
124
+ ${f.tool} mergetool --fallback "<your merge tool>" %b %t %y %r
125
+
126
+ (\`%b\` base, \`%t\` theirs, \`%y\` yours, \`%r\` result: adjust to your P4 client's
127
+ variable names; the argument ORDER is what matters.)
128
+ `,
129
+ plastic: `## Plastic SCM (Unity Version Control)
130
+
131
+ ${f.product} source shards are plain UTF-8 text; Plastic handles them as they
132
+ are. The \`ignore.conf\` (already emitted) keeps the packed document out of the
133
+ repository. For a lock-based workflow, configure exclusive checkout (lock.conf)
134
+ for the shard extensions.
135
+
136
+ In Preferences > Merge tools, add an external tool (a global entry is fine: the
137
+ wrapper looks at each file's name) with the arguments in BASE THEIRS OURS OUT
138
+ order:
139
+
140
+ ${f.tool} mergetool --fallback "<your merge tool>" @basefile @sourcefile @destinationfile @output
141
+ `,
142
+ svn: `## SVN
143
+
144
+ Set auto-props so ${f.product} source keeps LF (\`~/.subversion/config\` or the
145
+ repository's config):
146
+
147
+ [auto-props]
148
+ ${text.map((ext) => ` *.${ext} = svn:eol-style=LF`).join("\n")}
149
+ *.${f.pack} = svn:mime-type=application/octet-stream
150
+
151
+ SVN's ignore is a folder PROPERTY, not a file; set it from the project folder:
152
+
153
+ svn propset svn:ignore "*.${f.pack}${bundle === "ignore" ? `\\n*.${f.bundle}` : ""}\\n*.${f.conflict}" .
154
+
155
+ For a lock-based workflow, add \`svn:needs-lock\` to the shard patterns.
156
+
157
+ SVN allows one global merge tool. Point \`[helpers] merge-tool-cmd\` at a small
158
+ wrapper script that passes SVN's four arguments (base, theirs, mine, merged: BASE
159
+ THEIRS OURS OUT) to \`${f.tool} mergetool\`:
160
+
161
+ #!/bin/sh
162
+ exec ${f.tool} mergetool --fallback "<your merge tool>" "$1" "$2" "$3" "$4"
163
+ `
164
+ };
165
+ const head = vcs !== void 0 ? sections[vcs] : `No version control system chosen (\`${f.tool} init --vcs ${VCS_KINDS.join("|")}\` writes
166
+ the setup for one). Whichever you adopt, the rules are the same: ${f.product} source
167
+ is UTF-8 + LF text, and \`${f.tool} validate\` belongs in your pre-commit hook and
168
+ CI. Sections for every supported system:
169
+
170
+ ${VCS_KINDS.map((k) => sections[k]).join("\n")}`;
171
+ return `# Version-control setup for this ${f.product} project
172
+
173
+ ${head}
174
+ ${bundleSection(f, bundle)}`;
175
+ }
176
+ function bundleSection(f, bundle) {
177
+ const posture = bundle === "commit" ? `This project COMMITS the compiled \`.${f.bundle}\` bundle (\`${f.tool} init --bundle ignore\`
178
+ builds it in CI instead). The bundle is regenerated, never hand-merged: on a
179
+ conflict, keep ours and run \`${f.tool} export\` again. \`${f.tool} validate\` works out
180
+ the bundle's hash from source and FAILS if it is stale, so a forgotten rebuild
181
+ cannot ship unnoticed.` : `${IGNORES_MARK} \`.${f.bundle}\` bundle (built in CI; \`${f.tool} init
182
+ --bundle commit\` commits it instead). Run \`${f.tool} export\` as a build step.`;
183
+ return `## Compiled bundle and packed document
184
+
185
+ ${posture}
186
+
187
+ The packed \`.${f.pack}\` document (\`${f.tool} pack\`) is the send-and-return envelope
188
+ for collaborators without version control. It is a binary zip and a projection of
189
+ the shards, NOT source, so it stays out of version control (ignored above); edits
190
+ come back with \`${f.tool} unpack\`.
191
+ `;
192
+ }
193
+ function runMergetool(o) {
194
+ if (o.isSource(o.out) || o.isSource(o.ours)) return o.merge();
195
+ if (o.fallback === void 0) {
196
+ o.error(`${o.tool} mergetool: ${o.out} is not ${o.tool} source and no --fallback is configured`);
197
+ return 2;
198
+ }
199
+ const [cmd, ...pre] = o.fallback.split(/\s+/).filter(Boolean);
200
+ if (cmd === void 0) {
201
+ o.error(`${o.tool} mergetool: --fallback is empty`);
202
+ return 2;
203
+ }
204
+ const r = spawnSync(cmd, [...pre, o.base, o.theirs, o.ours, o.out], { stdio: "inherit" });
205
+ if (r.error) {
206
+ o.error(`${o.tool} mergetool: could not run the fallback '${cmd}': ${r.error.message}`);
207
+ return 1;
208
+ }
209
+ return r.status ?? 1;
210
+ }
211
+
212
+ export { VCS_KINDS, VCS_LABELS, VCS_SETUP_FILE, bundlePostureOf, ignoreFileFor, isVcsKind, runMergetool, vcsConfigFiles };
213
+ //# sourceMappingURL=vcs.js.map
214
+ //# sourceMappingURL=vcs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vcs.ts"],"names":[],"mappings":"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","file":"vcs.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Version-control setup for a project: the files `init` writes for the team's\n// version control system, the bundle posture, and the `mergetool` wrapper a\n// system with one global merge tool calls.\n//\n// Lifted from Patter (patter/packages/ops/src/init.ts and the CLI's\n// `mergetool`), where git, Perforce, Plastic SCM and SVN were all covered, so\n// the Storylet format takes the same support rather than a second copy of it\n// (patterkit design/family-parity-2026-10.md item 13). Each format says what\n// it is called and what its files are; the rules and the words are shared.\n//\n// A subpath of its own because `runMergetool` starts the fallback tool, which\n// needs `node:child_process`; the rest is plain text.\n// ---------------------------------------------------------------------------\n\nimport { spawnSync } from \"node:child_process\";\n\n/** The version control systems a project can be set up for. */\nexport const VCS_KINDS = [\"git\", \"perforce\", \"plastic\", \"svn\"] as const;\nexport type VcsKind = typeof VCS_KINDS[number];\n\nexport const isVcsKind = (value: unknown): value is VcsKind =>\n typeof value === \"string\" && (VCS_KINDS as readonly string[]).includes(value);\n\n/** What each system is called to a person, for pickers and messages. */\nexport const VCS_LABELS: Readonly<Record<VcsKind, string>> = {\n git: \"Git\", perforce: \"Perforce\", plastic: \"Plastic SCM\", svn: \"Subversion (SVN)\",\n};\n\n/** Whether the compiled bundle is committed, or built in CI and ignored. */\nexport type BundlePosture = \"commit\" | \"ignore\";\n\n/** What a format is called and what its files are. Extensions are without the dot. */\nexport interface VcsFormat {\n /** What the guide calls the source: \"Patter\", \"Storylet Studio\". */\n readonly product: string;\n /** The command-line tool: \"patter\", \"storyletengine\". */\n readonly tool: string;\n /** The git merge driver's name, as `.gitattributes` names it: \"patter\", \"storylets\". */\n readonly driver: string;\n /** The source shards' extensions: UTF-8 + LF text, merged by the structured merge. */\n readonly source: readonly string[];\n /** An extension manifest's suffix (`.extension.json`), and the folder that holds them: text, merged as text. */\n readonly manifest?: { readonly dir: string; readonly suffix: string };\n /** The compiled bundle's extension: regenerated, never hand-merged. */\n readonly bundle: string;\n /** The packed document's extension: a binary zip, kept out of version control. */\n readonly pack: string;\n /** The unresolved-merge sidecar's extension: never committed. */\n readonly conflict: string;\n /** Further binary paths for git, each with the line saying why (`assets/**`). */\n readonly binary?: readonly { readonly pattern: string; readonly note: string }[];\n}\n\n/** One file to write, relative to the project folder. */\nexport interface VcsFile {\n readonly path: string;\n readonly content: string;\n}\n\n/** The setup guide every project gets, whatever its system. */\nexport const VCS_SETUP_FILE = \"vcs-setup.md\";\n\n/** The ignore file a system reads, or undefined for one that keeps its ignores elsewhere (SVN: a property). */\nexport function ignoreFileFor(vcs: VcsKind | undefined): string | undefined {\n switch (vcs) {\n case \"git\": return \".gitignore\";\n case \"perforce\": return \".p4ignore\";\n case \"plastic\": return \"ignore.conf\";\n default: return undefined;\n }\n}\n\n/**\n * The files a project is set up with for `vcs`: the setup guide always, git's\n * `.gitattributes`, and the system's ignore file. With no system, the guide\n * carries every system's section and nothing else is written.\n *\n * Shared by `init` and an editor's switch of system, which writes these for the\n * new one and removes what the old one's list holds that this one's does not.\n */\nexport function vcsConfigFiles(format: VcsFormat, vcs: VcsKind | undefined, bundle: BundlePosture = \"commit\"): VcsFile[] {\n const files: VcsFile[] = [{ path: VCS_SETUP_FILE, content: vcsSetup(format, vcs, bundle) }];\n if (vcs === \"git\") files.push({ path: \".gitattributes\", content: gitAttributes(format) });\n const ignore = ignoreFileFor(vcs);\n if (ignore !== undefined) files.push({ path: ignore, content: ignoreContent(format, bundle) });\n return files;\n}\n\n/**\n * The bundle posture a project was set up with, read back from the files\n * `vcsConfigFiles` wrote: the ignore file naming the bundle, else the guide\n * saying the project ignores it (SVN keeps its ignores in a property). \"commit\"\n * when neither says so. `read` returns a project-relative file's text, or\n * undefined when it is not there.\n *\n * A switch of system re-writes the files, and always writing \"commit\" lost the\n * ignore rule of a project set up to ignore its bundle (Patter, before this).\n */\nexport function bundlePostureOf(format: VcsFormat, vcs: VcsKind | undefined, read: (path: string) => string | undefined): BundlePosture {\n const ignore = ignoreFileFor(vcs);\n if (ignore !== undefined && (read(ignore) ?? \"\").split(/\\r?\\n/).some((l) => l.trim() === `*.${format.bundle}`)) return \"ignore\";\n return (read(VCS_SETUP_FILE) ?? \"\").includes(IGNORES_MARK) ? \"ignore\" : \"commit\";\n}\n\n// --- the files -----------------------------------------------------------------\n\nconst IGNORES_MARK = \"This project IGNORES the compiled\";\n\nconst column = (patterns: readonly string[]): number => Math.max(...patterns.map((p) => p.length)) + 1;\n\nfunction gitAttributes(f: VcsFormat): string {\n const source = f.source.map((ext) => `*.${ext}`);\n const manifest = f.manifest !== undefined ? `${f.manifest.dir}/*${f.manifest.suffix}` : undefined;\n // Aligned on the extension patterns; a longer path pattern takes one space.\n const w = column([...source, `*.${f.bundle}`, `*.${f.pack}`]);\n const row = (pattern: string, attrs: string): string => `${pattern.padEnd(w)}${pattern.length >= w ? \" \" : \"\"}${attrs}`;\n const blocks = [\n `# ${f.product} source is UTF-8 + LF text (pinned; never let autocrlf touch it).\\n`\n + source.map((p) => row(p, \"text eol=lf\")).join(\"\\n\"),\n `# Id-keyed structured merge for ${f.product} source (the '${f.tool} merge' driver).\\n`\n + `# Register it once per clone (see ${VCS_SETUP_FILE}); until then git falls back to a\\n`\n + `# normal text merge for these: no harm, just not structure-aware.\\n`\n + source.map((p) => row(p, `merge=${f.driver}`)).join(\"\\n\"),\n ];\n if (manifest !== undefined) {\n blocks.push(`# An extension's manifest is the extension's own JSON, kept in the project so\\n`\n + `# every machine reads the same one, and never edited here: text, merged as text.\\n`\n + row(manifest, \"text eol=lf\"));\n }\n blocks.push(`# Generated artifacts. The compiled bundle is committed but REGENERATED, never\\n`\n + `# hand-merged: keep ours on conflict and rebuild ('${f.tool} validate' catches a\\n`\n + `# stale one). Needs a one-time 'git config merge.ours.driver true' (see ${VCS_SETUP_FILE}).\\n`\n + row(`*.${f.bundle}`, \"text eol=lf merge=ours\") + \"\\n\"\n + `# The packed document is a binary zip envelope, kept out of version control (.gitignore).\\n`\n + row(`*.${f.pack}`, \"binary\"));\n for (const b of f.binary ?? []) blocks.push(`${b.note.split(\"\\n\").map((l) => `# ${l}`).join(\"\\n\")}\\n${row(b.pattern, \"binary\")}`);\n return blocks.join(\"\\n\\n\") + \"\\n\";\n}\n\nfunction ignoreContent(f: VcsFormat, bundle: BundlePosture): string {\n const lines = [\n `# ${f.product} generated artifacts: not source (see ${VCS_SETUP_FILE}).`,\n \"# Packed document (the send-and-return envelope):\",\n `*.${f.pack}`,\n \"# Unresolved merge sidecar (a lingering one is a validate error). The merge also names its clashes\",\n \"# in the shard itself, under \\\"unresolved\\\", which IS committed with the file, so CI refuses an\",\n \"# unresolved merge wherever it goes:\",\n `*.${f.conflict}`,\n ];\n if (bundle === \"ignore\") lines.push(\"# Compiled bundle (built in CI, not committed under this posture):\", `*.${f.bundle}`);\n return lines.join(\"\\n\") + \"\\n\";\n}\n\nfunction vcsSetup(f: VcsFormat, vcs: VcsKind | undefined, bundle: BundlePosture): string {\n const text = [...f.source, ...(f.manifest !== undefined ? [f.manifest.suffix.replace(/^\\./, \"\")] : []), f.bundle];\n const sections: Record<VcsKind, string> = {\n git: `## git\n\n\\`.gitattributes\\` (already emitted) pins ${f.product} source shards to UTF-8 + LF\ntext and marks the generated artifacts (the \\`.${f.bundle}\\` bundle \\`merge=ours\\`, the\n\\`.${f.pack}\\` document \\`binary\\`). \\`.gitignore\\` (already emitted) keeps the document\n(and the bundle, under the \"ignore\" posture) out of source control.\n\nRecommended pre-commit hook (\\`.git/hooks/pre-commit\\`, executable):\n\n #!/bin/sh\n ${f.tool} validate || exit 1\n\nThe \\`merge=${f.driver}\\` rules in \\`.gitattributes\\` are already active; register the\ndrivers once per clone (git config is not repo-tracked). Until then git falls\nback to a normal text merge for those files:\n\n git config merge.${f.driver}.name \"${f.product} structured merge\"\n git config merge.${f.driver}.driver \"${f.tool} merge %O %A %B -o %A --path %P\"\n git config merge.ours.driver true\n\ngit calls the per-path driver directly (no \\`mergetool\\` wrapper needed). \\`%O %A %B\\`\nare base, ours and theirs; the merged result is written back to \\`%A\\`, and \\`%P\\` is\nthe file's real path. On a conflict \\`${f.tool} merge\\` exits non-zero and writes a\n\\`.${f.conflict}\\` sidecar beside that file, so the merge stays unresolved.\n`,\n perforce: `## Perforce\n\nAdd ${f.product}'s extensions to the typemap (\\`p4 typemap\\`): source shards and\nthe compiled bundle are TEXT with LF; the packed document is BINARY:\n\n${text.map((ext) => ` text //....${ext}`).join(\"\\n\")}\n binary //....${f.pack}\n\nOn a unicode-mode server, set \\`P4CHARSET=utf8\\`. For a lock-based workflow (one\nlock per file), add \\`+l\\` to make checkouts exclusive. A \\`.p4ignore\\` (already\nemitted) keeps the packed document out of the depot.\n\nPerforce allows ONE global merge tool, so set \\`${f.tool} mergetool\\` as it: it\nruns the structured merge for ${f.product} source and hands everything else to\nyour normal tool. Map Perforce's variables into the order BASE THEIRS OURS OUT:\n\n ${f.tool} mergetool --fallback \"<your merge tool>\" %b %t %y %r\n\n(\\`%b\\` base, \\`%t\\` theirs, \\`%y\\` yours, \\`%r\\` result: adjust to your P4 client's\nvariable names; the argument ORDER is what matters.)\n`,\n plastic: `## Plastic SCM (Unity Version Control)\n\n${f.product} source shards are plain UTF-8 text; Plastic handles them as they\nare. The \\`ignore.conf\\` (already emitted) keeps the packed document out of the\nrepository. For a lock-based workflow, configure exclusive checkout (lock.conf)\nfor the shard extensions.\n\nIn Preferences > Merge tools, add an external tool (a global entry is fine: the\nwrapper looks at each file's name) with the arguments in BASE THEIRS OURS OUT\norder:\n\n ${f.tool} mergetool --fallback \"<your merge tool>\" @basefile @sourcefile @destinationfile @output\n`,\n svn: `## SVN\n\nSet auto-props so ${f.product} source keeps LF (\\`~/.subversion/config\\` or the\nrepository's config):\n\n [auto-props]\n${text.map((ext) => ` *.${ext} = svn:eol-style=LF`).join(\"\\n\")}\n *.${f.pack} = svn:mime-type=application/octet-stream\n\nSVN's ignore is a folder PROPERTY, not a file; set it from the project folder:\n\n svn propset svn:ignore \"*.${f.pack}${bundle === \"ignore\" ? `\\\\n*.${f.bundle}` : \"\"}\\\\n*.${f.conflict}\" .\n\nFor a lock-based workflow, add \\`svn:needs-lock\\` to the shard patterns.\n\nSVN allows one global merge tool. Point \\`[helpers] merge-tool-cmd\\` at a small\nwrapper script that passes SVN's four arguments (base, theirs, mine, merged: BASE\nTHEIRS OURS OUT) to \\`${f.tool} mergetool\\`:\n\n #!/bin/sh\n exec ${f.tool} mergetool --fallback \"<your merge tool>\" \"$1\" \"$2\" \"$3\" \"$4\"\n`,\n };\n const head = vcs !== undefined\n ? sections[vcs]\n : `No version control system chosen (\\`${f.tool} init --vcs ${VCS_KINDS.join(\"|\")}\\` writes\nthe setup for one). Whichever you adopt, the rules are the same: ${f.product} source\nis UTF-8 + LF text, and \\`${f.tool} validate\\` belongs in your pre-commit hook and\nCI. Sections for every supported system:\n\n${VCS_KINDS.map((k) => sections[k]).join(\"\\n\")}`;\n return `# Version-control setup for this ${f.product} project\\n\\n${head}\\n${bundleSection(f, bundle)}`;\n}\n\nfunction bundleSection(f: VcsFormat, bundle: BundlePosture): string {\n const posture = bundle === \"commit\"\n ? `This project COMMITS the compiled \\`.${f.bundle}\\` bundle (\\`${f.tool} init --bundle ignore\\`\nbuilds it in CI instead). The bundle is regenerated, never hand-merged: on a\nconflict, keep ours and run \\`${f.tool} export\\` again. \\`${f.tool} validate\\` works out\nthe bundle's hash from source and FAILS if it is stale, so a forgotten rebuild\ncannot ship unnoticed.`\n : `${IGNORES_MARK} \\`.${f.bundle}\\` bundle (built in CI; \\`${f.tool} init\n--bundle commit\\` commits it instead). Run \\`${f.tool} export\\` as a build step.`;\n return `## Compiled bundle and packed document\n\n${posture}\n\nThe packed \\`.${f.pack}\\` document (\\`${f.tool} pack\\`) is the send-and-return envelope\nfor collaborators without version control. It is a binary zip and a projection of\nthe shards, NOT source, so it stays out of version control (ignored above); edits\ncome back with \\`${f.tool} unpack\\`.\n`;\n}\n\n// --- mergetool -------------------------------------------------------------------\n\nexport interface MergetoolOptions {\n /** The four files, in the order every system with one global tool passes them. */\n readonly base: string;\n readonly theirs: string;\n readonly ours: string;\n readonly out: string;\n /** The team's ordinary merge tool, with any flags of its own (\"code --wait --merge\"). */\n readonly fallback?: string;\n /** Is this path the format's source? Asked of `out` and of `ours`. */\n readonly isSource: (path: string) => boolean;\n /** Run the structured merge of base, ours and theirs into `out`; its exit code. */\n readonly merge: () => number;\n /** Where to say what went wrong. */\n readonly error: (message: string) => void;\n /** The tool's name, for messages: \"patter\", \"storyletengine\". */\n readonly tool: string;\n}\n\n/**\n * The sniff-and-dispatch wrapper: one global external merge tool serves the\n * whole depot (Perforce, Plastic SCM and SVN allow only one). The format's\n * source goes to the structured merge; anything else goes to `fallback`, with\n * the four paths as separate arguments and no shell, so spaces and\n * metacharacters in them are safe. git does not need this: it calls the\n * per-path driver directly.\n *\n * Exit codes: the merge's, the fallback's, 1 when the fallback could not\n * start, 2 when a file is not the format's and no fallback is configured.\n */\nexport function runMergetool(o: MergetoolOptions): number {\n if (o.isSource(o.out) || o.isSource(o.ours)) return o.merge();\n if (o.fallback === undefined) {\n o.error(`${o.tool} mergetool: ${o.out} is not ${o.tool} source and no --fallback is configured`);\n return 2;\n }\n const [cmd, ...pre] = o.fallback.split(/\\s+/).filter(Boolean);\n if (cmd === undefined) {\n o.error(`${o.tool} mergetool: --fallback is empty`);\n return 2;\n }\n const r = spawnSync(cmd, [...pre, o.base, o.theirs, o.ours, o.out], { stdio: \"inherit\" });\n // A tool that could not start (not installed, not on PATH) has no status: say so, or the merge just fails.\n if (r.error) {\n o.error(`${o.tool} mergetool: could not run the fallback '${cmd}': ${r.error.message}`);\n return 1;\n }\n return r.status ?? 1;\n}\n"]}
package/extensions.md CHANGED
@@ -160,7 +160,7 @@ manifest from the working tree.
160
160
 
161
161
  ### The engine
162
162
 
163
- `mergeTree(strategy, base, ours, theirs)` is the three-way merge both formats' shards use, a tree of five
163
+ `mergeTree(strategy, base, ours, theirs)` is the three-way merge both formats' shards use, a tree of six
164
164
  behaviours (lifted from the Storylets format's strategy table):
165
165
 
166
166
  - `atomic`: one value, classic three-way.
@@ -170,6 +170,11 @@ behaviours (lifted from the Storylets format's strategy table):
170
170
  twice with different content are clashes. `order: "sorted"` (the default) sorts by key; `"preserve"` keeps the
171
171
  authored order, taking the side that reordered, and clashes when both did.
172
172
  - `object(fields)`: named keys get their own strategy, every other key is atomic.
173
+ - `comments`: a list of comment threads, keyed by id, merged message by message (`mergeComments`). Messages are
174
+ keyed by author and timestamp and are the union of both sides', oldest first; a withdrawn message stays
175
+ withdrawn, and one a side removed, or lost with its thread, counts as withdrawn by that side; one message edited
176
+ two ways clashes. A thread's own fields merge one by one, and a thread with nothing readable left goes. Threads
177
+ come back oldest first.
173
178
 
174
179
  ### The unresolved mark
175
180
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildwinter/toolkit",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Small shared pieces of the authoring tooling: opaque ids, a stable hash, JSON5 source parsing, find/replace matching, the archive guards, and (in ./extensions) the spec and merge for Patter extensions and Storylet extensions. Nothing here runs in a game.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,7 +17,8 @@
17
17
  "zip",
18
18
  "tooling",
19
19
  "extensions",
20
- "merge"
20
+ "merge",
21
+ "version-control"
21
22
  ],
22
23
  "main": "./dist/index.cjs",
23
24
  "module": "./dist/index.js",
@@ -37,6 +38,11 @@
37
38
  "types": "./dist/extensions.d.ts",
38
39
  "import": "./dist/extensions.js",
39
40
  "require": "./dist/extensions.cjs"
41
+ },
42
+ "./vcs": {
43
+ "types": "./dist/vcs.d.ts",
44
+ "import": "./dist/vcs.js",
45
+ "require": "./dist/vcs.cjs"
40
46
  }
41
47
  },
42
48
  "files": [