spawnpack 0.1.9 → 0.1.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/dist/spawnpack.js +242 -36
- package/package.json +2 -1
- package/templates/AGENTS.md +32 -22
- package/templates/CLAUDE.md +32 -22
- package/templates/filters/typegen/main.js +86 -0
package/dist/spawnpack.js
CHANGED
|
@@ -1539,7 +1539,9 @@ import { join } from "node:path";
|
|
|
1539
1539
|
var VERSIONS = {
|
|
1540
1540
|
minEngineVersion: [1, 26, 0],
|
|
1541
1541
|
manifestFormat: 2,
|
|
1542
|
-
|
|
1542
|
+
rolldown: "^1.0.0-beta.56",
|
|
1543
|
+
esToolkit: "^1.43.0",
|
|
1544
|
+
jsoncParser: "^3.3.1"
|
|
1543
1545
|
};
|
|
1544
1546
|
var DEFAULT_MINECRAFT_DEPENDENCY_VERSIONS = {
|
|
1545
1547
|
server: "2.6.0",
|
|
@@ -1744,7 +1746,7 @@ function generatePackageJson(config, versions) {
|
|
|
1744
1746
|
if (!config.useRgl) {
|
|
1745
1747
|
scripts.build = "tsc --project tsconfig.json";
|
|
1746
1748
|
}
|
|
1747
|
-
scripts.typecheck = "tsc --project tsconfig.json --noEmit";
|
|
1749
|
+
scripts.typecheck = config.useRgl ? "tsc --project data/scripts/tsconfig.json --noEmit" : "tsc --project tsconfig.json --noEmit";
|
|
1748
1750
|
scripts.format = "dprint fmt";
|
|
1749
1751
|
}
|
|
1750
1752
|
if (config.scripting === "javascript") {
|
|
@@ -1754,6 +1756,7 @@ function generatePackageJson(config, versions) {
|
|
|
1754
1756
|
name: `${config.identifier}-${config.projectId}`,
|
|
1755
1757
|
version: PACK_VERSION2,
|
|
1756
1758
|
private: true,
|
|
1759
|
+
...config.useRgl ? { workspaces: ["./filters/*"] } : {},
|
|
1757
1760
|
type: "module",
|
|
1758
1761
|
scripts,
|
|
1759
1762
|
dependencies: getSelectedScriptDependencies(config, versions)
|
|
@@ -1815,7 +1818,7 @@ function generateDprintConfig() {
|
|
|
1815
1818
|
}
|
|
1816
1819
|
function generateRglConfig(config) {
|
|
1817
1820
|
return {
|
|
1818
|
-
$schema: "https://raw.githubusercontent.com/ink0rr/rgl-schemas/main/config/v1.
|
|
1821
|
+
$schema: "https://raw.githubusercontent.com/ink0rr/rgl-schemas/main/config/v1.1.json",
|
|
1819
1822
|
author: config.author,
|
|
1820
1823
|
name: config.projectName,
|
|
1821
1824
|
packs: {
|
|
@@ -1825,41 +1828,93 @@ function generateRglConfig(config) {
|
|
|
1825
1828
|
regolith: {
|
|
1826
1829
|
dataPath: "./data",
|
|
1827
1830
|
filterDefinitions: {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
+
typegen: {
|
|
1832
|
+
runWith: "nodejs",
|
|
1833
|
+
script: "./filters/typegen/main.js"
|
|
1834
|
+
},
|
|
1835
|
+
rolldown: {
|
|
1836
|
+
runWith: "nodejs",
|
|
1837
|
+
script: "./filters/rolldown/main.js"
|
|
1831
1838
|
}
|
|
1832
1839
|
},
|
|
1833
1840
|
profiles: {
|
|
1834
1841
|
default: {
|
|
1835
1842
|
export: { target: "development" },
|
|
1836
1843
|
filters: [
|
|
1837
|
-
{
|
|
1838
|
-
|
|
1839
|
-
settings: {
|
|
1840
|
-
outfile: `./BP/${getScriptEntryPath(config)}`,
|
|
1841
|
-
sourcemap: true,
|
|
1842
|
-
minify: false
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1844
|
+
{ filter: "typegen" },
|
|
1845
|
+
{ filter: "rolldown" }
|
|
1845
1846
|
]
|
|
1846
1847
|
},
|
|
1847
1848
|
build: {
|
|
1848
1849
|
export: { target: "local" },
|
|
1849
1850
|
filters: [
|
|
1850
|
-
{
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1851
|
+
{ filter: "typegen" },
|
|
1852
|
+
{ filter: "rolldown", arguments: ["--prod"] }
|
|
1853
|
+
]
|
|
1854
|
+
},
|
|
1855
|
+
preview: {
|
|
1856
|
+
export: { target: "development" },
|
|
1857
|
+
filters: [
|
|
1858
|
+
{ profile: "build" }
|
|
1857
1859
|
]
|
|
1858
1860
|
}
|
|
1859
1861
|
}
|
|
1860
1862
|
}
|
|
1861
1863
|
};
|
|
1862
1864
|
}
|
|
1865
|
+
function getRolldownExternalModules(config) {
|
|
1866
|
+
return ["@minecraft/server", ...config.scriptPackages.serverUi ? ["@minecraft/server-ui"] : []];
|
|
1867
|
+
}
|
|
1868
|
+
function generateRolldownFilterMain(config) {
|
|
1869
|
+
const external = getRolldownExternalModules(config).map((moduleName) => JSON.stringify(moduleName)).join(", ");
|
|
1870
|
+
const outfile = `./BP/${getScriptEntryPath(config)}`;
|
|
1871
|
+
return `import { build, defineConfig } from "rolldown";
|
|
1872
|
+
|
|
1873
|
+
const arg = process.argv[2];
|
|
1874
|
+
|
|
1875
|
+
const config = defineConfig({
|
|
1876
|
+
external: [${external}],
|
|
1877
|
+
input: "./data/scripts/main.ts",
|
|
1878
|
+
output: {
|
|
1879
|
+
format: "esm",
|
|
1880
|
+
file: ${JSON.stringify(outfile)},
|
|
1881
|
+
sourcemap: true,
|
|
1882
|
+
},
|
|
1883
|
+
tsconfig: "./data/scripts/tsconfig.json",
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
if (arg === "--prod") {
|
|
1887
|
+
config.output.minify = true;
|
|
1888
|
+
config.output.sourcemap = false;
|
|
1889
|
+
config.transform = {
|
|
1890
|
+
dropLabels: ["DEBUG"],
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
await build(config);
|
|
1895
|
+
`;
|
|
1896
|
+
}
|
|
1897
|
+
function generateRolldownFilterPackageJson() {
|
|
1898
|
+
return {
|
|
1899
|
+
name: "rolldown-filter",
|
|
1900
|
+
private: true,
|
|
1901
|
+
type: "module",
|
|
1902
|
+
dependencies: {
|
|
1903
|
+
rolldown: VERSIONS.rolldown
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
function generateTypegenFilterPackageJson() {
|
|
1908
|
+
return {
|
|
1909
|
+
name: "typegen",
|
|
1910
|
+
private: true,
|
|
1911
|
+
type: "module",
|
|
1912
|
+
dependencies: {
|
|
1913
|
+
"es-toolkit": VERSIONS.esToolkit,
|
|
1914
|
+
"jsonc-parser": VERSIONS.jsoncParser
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1863
1918
|
function generateMainTs(config) {
|
|
1864
1919
|
return `import { world } from "@minecraft/server";
|
|
1865
1920
|
|
|
@@ -1882,11 +1937,74 @@ function generateLangFile(projectName, packType) {
|
|
|
1882
1937
|
pack.description=Generated with Spawnpack
|
|
1883
1938
|
`;
|
|
1884
1939
|
}
|
|
1885
|
-
function
|
|
1886
|
-
return
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1940
|
+
function generateScriptsTsConfig() {
|
|
1941
|
+
return {
|
|
1942
|
+
compilerOptions: {
|
|
1943
|
+
lib: [
|
|
1944
|
+
"ES2015",
|
|
1945
|
+
"ES2016.Array.Include",
|
|
1946
|
+
"ES2017.ArrayBuffer",
|
|
1947
|
+
"ES2017.Date",
|
|
1948
|
+
"ES2017.Object",
|
|
1949
|
+
"ES2017.String",
|
|
1950
|
+
"ES2017.TypedArrays",
|
|
1951
|
+
"ES2018.AsyncIterable",
|
|
1952
|
+
"ES2018.AsyncGenerator",
|
|
1953
|
+
"ES2018.Promise",
|
|
1954
|
+
"ES2018.Regexp",
|
|
1955
|
+
"ES2019.Array",
|
|
1956
|
+
"ES2019.Object",
|
|
1957
|
+
"ES2019.String",
|
|
1958
|
+
"ES2019.Symbol",
|
|
1959
|
+
"ES2020.BigInt",
|
|
1960
|
+
"ES2020.Date",
|
|
1961
|
+
"ES2020.Number",
|
|
1962
|
+
"ES2015.Promise",
|
|
1963
|
+
"ES2020.String",
|
|
1964
|
+
"ES2020.Promise",
|
|
1965
|
+
"ES2020.Symbol.WellKnown",
|
|
1966
|
+
"ES2021.Promise",
|
|
1967
|
+
"ES2021.String",
|
|
1968
|
+
"ES2022.Array",
|
|
1969
|
+
"ES2022.Error",
|
|
1970
|
+
"ES2022.Object",
|
|
1971
|
+
"ES2022.RegExp",
|
|
1972
|
+
"ES2022.String",
|
|
1973
|
+
"ES2023.Array",
|
|
1974
|
+
"ES2023.Collection",
|
|
1975
|
+
"ES2024.ArrayBuffer",
|
|
1976
|
+
"ES2024.Collection",
|
|
1977
|
+
"ES2024.Object",
|
|
1978
|
+
"ES2024.Promise",
|
|
1979
|
+
"ES2024.Regexp",
|
|
1980
|
+
"ES2024.String"
|
|
1981
|
+
],
|
|
1982
|
+
target: "es2024",
|
|
1983
|
+
module: "es2022",
|
|
1984
|
+
moduleDetection: "force",
|
|
1985
|
+
allowJs: true,
|
|
1986
|
+
moduleResolution: "bundler",
|
|
1987
|
+
noEmit: true,
|
|
1988
|
+
strict: true,
|
|
1989
|
+
skipLibCheck: true,
|
|
1990
|
+
noFallthroughCasesInSwitch: true,
|
|
1991
|
+
noImplicitOverride: true,
|
|
1992
|
+
noUnusedLocals: true,
|
|
1993
|
+
noUnusedParameters: true,
|
|
1994
|
+
paths: { "~/*": ["./*"] }
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function generateGitignore(config) {
|
|
1999
|
+
const lines = ["node_modules/", "dist/", ".logs/", "*.zip"];
|
|
2000
|
+
if (config.useRgl) {
|
|
2001
|
+
lines.push("/.regolith", "/build");
|
|
2002
|
+
}
|
|
2003
|
+
if (config.useRgl && config.scripting === "typescript") {
|
|
2004
|
+
lines.push("data/scripts/generated_types.ts");
|
|
2005
|
+
}
|
|
2006
|
+
return `${lines.join(`
|
|
2007
|
+
`)}
|
|
1890
2008
|
`;
|
|
1891
2009
|
}
|
|
1892
2010
|
function generateReadme(config) {
|
|
@@ -1895,13 +2013,14 @@ function generateReadme(config) {
|
|
|
1895
2013
|
const installStep = config.scripting !== "none" ? "1. Install dependencies with `bun install` or `npm install`." : "1. No package install is required for this scaffold.";
|
|
1896
2014
|
const buildSteps = config.scripting === "none" ? "2. Edit your BP/RP JSON files directly inside `packs/`." : config.useRgl ? "2. Run `rgl watch` while developing to rebuild and export your add-on automatically.\n3. Run `rgl run build` when you want a production build." : config.scripting === "typescript" ? "2. Write your gameplay scripts in `data/scripts/`.\n3. Run `bun run build` or `npm run build` to compile TypeScript into `packs/BP/scripts/`." : "2. Write your gameplay scripts in `packs/BP/scripts/` and let Minecraft copy the pack into `com.mojang`.";
|
|
1897
2015
|
const aiDocFilename = getAiDocFilename(config.aiSetup);
|
|
2016
|
+
const ponytailAiLine = config.installPonytail ? config.aiSetup === "other" ? "Ponytail is enabled via `opencode.json`.\n\n" : "To enable ponytail in Claude Code, run `/plugin marketplace add DietrichGebert/ponytail` then `/plugin install ponytail@ponytail`.\n\n" : "";
|
|
1898
2017
|
const aiSection = aiDocFilename !== null ? `## AI Tooling
|
|
1899
2018
|
|
|
1900
2019
|
Spawnpack generated \`${aiDocFilename}\` and \`.mcp.json\` for AI-assisted development. The configured MCP servers work out of the box — no API keys required.
|
|
1901
2020
|
|
|
1902
2021
|
Exa's hosted MCP runs on a rate-limited free tier. If you need higher limits, you can add your own Exa API key to \`.mcp.json\` (optional): https://dashboard.exa.ai/api-keys
|
|
1903
2022
|
|
|
1904
|
-
` : "";
|
|
2023
|
+
${ponytailAiLine}` : "";
|
|
1905
2024
|
const scriptPackageSection = config.scripting !== "none" ? `## Script Packages
|
|
1906
2025
|
|
|
1907
2026
|
Selected npm packages are installed through \`package.json\`. Only runtime Script API modules such as \`@minecraft/server\` and \`@minecraft/server-ui\` are written to \`packs/BP/manifest.json\`; npm-side libraries such as \`@minecraft/vanilla-data\` and \`@minecraft/math\` are imported and bundled from \`package.json\`.
|
|
@@ -1935,6 +2054,7 @@ ${scriptPackageSection}${config.useRgl ? `## rgl Workflow
|
|
|
1935
2054
|
- \`rgl watch\` rebuilds your scripts and exports your add-on while you work.
|
|
1936
2055
|
- \`rgl run\` builds a development export once.
|
|
1937
2056
|
- \`rgl run build\` creates a production-ready build.
|
|
2057
|
+
- \`typegen\` auto-generates typed custom IDs (\`BlockId\`/\`EntityId\`/\`ItemId\`) into \`data/scripts/generated_types.ts\` on each build.
|
|
1938
2058
|
|
|
1939
2059
|
The generated \`config.json\` already points the script bundle to \`${runtimeEntry}\`.
|
|
1940
2060
|
|
|
@@ -1970,6 +2090,10 @@ function generateTerrainTexture(config) {
|
|
|
1970
2090
|
}
|
|
1971
2091
|
|
|
1972
2092
|
// src/engine/generator.ts
|
|
2093
|
+
var TYPEGEN_FILTER_TEMPLATE_URLS = [
|
|
2094
|
+
new URL("../../templates/filters/typegen/main.js", import.meta.url),
|
|
2095
|
+
new URL("../templates/filters/typegen/main.js", import.meta.url)
|
|
2096
|
+
];
|
|
1973
2097
|
var BP_NAMESPACED_DIRECTORIES = [
|
|
1974
2098
|
"animation_controllers",
|
|
1975
2099
|
"animations",
|
|
@@ -2009,6 +2133,16 @@ async function writeJsonFile(filePath, value) {
|
|
|
2009
2133
|
async function writeTextFile(filePath, value) {
|
|
2010
2134
|
await writeFile(filePath, value, "utf8");
|
|
2011
2135
|
}
|
|
2136
|
+
async function createTypegenFilterMain() {
|
|
2137
|
+
for (const templateUrl of TYPEGEN_FILTER_TEMPLATE_URLS) {
|
|
2138
|
+
const templateFile = Bun.file(templateUrl);
|
|
2139
|
+
const exists = await templateFile.exists().then((value) => value, () => false);
|
|
2140
|
+
if (exists) {
|
|
2141
|
+
return await templateFile.text();
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
return await Bun.file(TYPEGEN_FILTER_TEMPLATE_URLS[0]).text();
|
|
2145
|
+
}
|
|
2012
2146
|
function getScopedContentDirectory(basePath, folder, config) {
|
|
2013
2147
|
return join(basePath, folder, ...getMarketplaceScopeSegments(config));
|
|
2014
2148
|
}
|
|
@@ -2069,6 +2203,9 @@ async function generateProject(config) {
|
|
|
2069
2203
|
directories.push(join(bpPath, "scripts"), join(bpPath, "scripts", config.identifier), bpScriptPath);
|
|
2070
2204
|
}
|
|
2071
2205
|
}
|
|
2206
|
+
if (config.useRgl && config.scripting !== "none") {
|
|
2207
|
+
directories.push(join(destination, "filters"), join(destination, "filters", "rolldown"), join(destination, "filters", "typegen"));
|
|
2208
|
+
}
|
|
2072
2209
|
await Promise.all(directories.filter(shouldCreateDirectory).map((directory) => mkdir(directory, { recursive: true })));
|
|
2073
2210
|
const bpUuid = crypto.randomUUID();
|
|
2074
2211
|
const rpUuid = crypto.randomUUID();
|
|
@@ -2086,7 +2223,7 @@ async function generateProject(config) {
|
|
|
2086
2223
|
writeTextFile(join(rpPath, "textures", "flipbook_textures.json"), "[]"),
|
|
2087
2224
|
writeTextFile(join(bpPath, "texts", "languages.json"), '["en_US"]'),
|
|
2088
2225
|
writeTextFile(join(rpPath, "texts", "languages.json"), '["en_US"]'),
|
|
2089
|
-
writeTextFile(join(destination, ".gitignore"), generateGitignore()),
|
|
2226
|
+
writeTextFile(join(destination, ".gitignore"), generateGitignore(config)),
|
|
2090
2227
|
writeTextFile(join(destination, "README.md"), generateReadme(config))
|
|
2091
2228
|
];
|
|
2092
2229
|
if (config.scripting !== "none") {
|
|
@@ -2096,10 +2233,11 @@ async function generateProject(config) {
|
|
|
2096
2233
|
writes.push(writeTextFile(join(bpScriptPath, "main.js"), generateMainJs(config)));
|
|
2097
2234
|
}
|
|
2098
2235
|
if (config.scripting === "typescript") {
|
|
2099
|
-
|
|
2236
|
+
const tsconfigWrite = config.useRgl ? writeJsonFile(join(dataScriptsPath, "tsconfig.json"), generateScriptsTsConfig()) : writeJsonFile(join(destination, "tsconfig.json"), generateTsConfig(config));
|
|
2237
|
+
writes.push(tsconfigWrite, writeJsonFile(join(destination, "dprint.json"), generateDprintConfig()), writeTextFile(join(dataScriptsPath, "main.ts"), generateMainTs(config)));
|
|
2100
2238
|
}
|
|
2101
2239
|
if (config.scripting !== "none" && config.useRgl) {
|
|
2102
|
-
writes.push(writeJsonFile(join(destination, "config.json"), generateRglConfig(config)));
|
|
2240
|
+
writes.push(writeJsonFile(join(destination, "config.json"), generateRglConfig(config)), writeTextFile(join(destination, "filters", "rolldown", "main.js"), generateRolldownFilterMain(config)), writeJsonFile(join(destination, "filters", "rolldown", "package.json"), generateRolldownFilterPackageJson()), writeJsonFile(join(destination, "filters", "typegen", "package.json"), generateTypegenFilterPackageJson()), writeTextFile(join(destination, "filters", "typegen", "main.js"), await createTypegenFilterMain()));
|
|
2103
2241
|
}
|
|
2104
2242
|
await Promise.all(writes);
|
|
2105
2243
|
}
|
|
@@ -2142,6 +2280,7 @@ var AI_EXCLUDE_LINES = [
|
|
|
2142
2280
|
"/CLAUDE.md",
|
|
2143
2281
|
"/AGENTS.md",
|
|
2144
2282
|
"/.mcp.json",
|
|
2283
|
+
"/opencode.json",
|
|
2145
2284
|
"/.claude/"
|
|
2146
2285
|
];
|
|
2147
2286
|
function getTemplateUrls(filename) {
|
|
@@ -2201,13 +2340,42 @@ async function setupGitExcludes(projectPath) {
|
|
|
2201
2340
|
return;
|
|
2202
2341
|
});
|
|
2203
2342
|
}
|
|
2204
|
-
async function
|
|
2343
|
+
async function writeOpencodePluginConfig(projectPath) {
|
|
2344
|
+
const configPath = join3(projectPath, "opencode.json");
|
|
2345
|
+
const pluginId = "@dietrichgebert/ponytail";
|
|
2346
|
+
const file = Bun.file(configPath);
|
|
2347
|
+
const exists = await file.exists().then((value) => value, () => false);
|
|
2348
|
+
let nextConfig = {
|
|
2349
|
+
$schema: "https://opencode.ai/config.json",
|
|
2350
|
+
plugin: [pluginId]
|
|
2351
|
+
};
|
|
2352
|
+
if (exists) {
|
|
2353
|
+
try {
|
|
2354
|
+
const parsed = await file.json();
|
|
2355
|
+
const plugins = Array.isArray(parsed.plugin) ? [...parsed.plugin] : [];
|
|
2356
|
+
if (!plugins.includes(pluginId)) {
|
|
2357
|
+
plugins.push(pluginId);
|
|
2358
|
+
}
|
|
2359
|
+
nextConfig = { ...parsed, plugin: plugins };
|
|
2360
|
+
} catch {}
|
|
2361
|
+
}
|
|
2362
|
+
await Bun.write(configPath, `${JSON.stringify(nextConfig, null, 4)}
|
|
2363
|
+
`).then(() => {
|
|
2364
|
+
return;
|
|
2365
|
+
}, () => {
|
|
2366
|
+
return;
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
async function setupAi(projectPath, aiSetup, installPonytail) {
|
|
2205
2370
|
if (aiSetup === "none") {
|
|
2206
2371
|
return;
|
|
2207
2372
|
}
|
|
2208
2373
|
await generateAiDoc(projectPath, aiSetup);
|
|
2209
2374
|
await setupGitExcludes(projectPath);
|
|
2210
2375
|
await generateMcpConfig(projectPath);
|
|
2376
|
+
if (installPonytail && aiSetup === "other") {
|
|
2377
|
+
await writeOpencodePluginConfig(projectPath);
|
|
2378
|
+
}
|
|
2211
2379
|
}
|
|
2212
2380
|
|
|
2213
2381
|
// src/provisioning/tools.ts
|
|
@@ -2405,8 +2573,20 @@ function showFolderTree(config) {
|
|
|
2405
2573
|
lines.push(import_picocolors.default.dim("│ └─ manifest.json"));
|
|
2406
2574
|
if (config.scripting === "typescript") {
|
|
2407
2575
|
lines.push(import_picocolors.default.dim("├─ data/scripts/"));
|
|
2408
|
-
|
|
2409
|
-
|
|
2576
|
+
if (config.useRgl) {
|
|
2577
|
+
lines.push(import_picocolors.default.dim("│ ├─ main.ts"));
|
|
2578
|
+
lines.push(import_picocolors.default.dim("│ └─ tsconfig.json"));
|
|
2579
|
+
lines.push(import_picocolors.default.dim("├─ filters/"));
|
|
2580
|
+
lines.push(import_picocolors.default.dim("│ ├─ rolldown/"));
|
|
2581
|
+
lines.push(import_picocolors.default.dim("│ │ ├─ main.js"));
|
|
2582
|
+
lines.push(import_picocolors.default.dim("│ │ └─ package.json"));
|
|
2583
|
+
lines.push(import_picocolors.default.dim("│ └─ typegen/"));
|
|
2584
|
+
lines.push(import_picocolors.default.dim("│ ├─ main.js"));
|
|
2585
|
+
lines.push(import_picocolors.default.dim("│ └─ package.json"));
|
|
2586
|
+
} else {
|
|
2587
|
+
lines.push(import_picocolors.default.dim("│ └─ main.ts"));
|
|
2588
|
+
lines.push(import_picocolors.default.dim("├─ tsconfig.json"));
|
|
2589
|
+
}
|
|
2410
2590
|
lines.push(import_picocolors.default.dim("├─ dprint.json"));
|
|
2411
2591
|
}
|
|
2412
2592
|
if (config.scripting === "javascript") {
|
|
@@ -2422,6 +2602,9 @@ function showFolderTree(config) {
|
|
|
2422
2602
|
if (aiDocFilename !== null) {
|
|
2423
2603
|
lines.push(import_picocolors.default.dim(`├─ ${aiDocFilename}`));
|
|
2424
2604
|
lines.push(import_picocolors.default.dim("├─ .mcp.json"));
|
|
2605
|
+
if (config.installPonytail && config.aiSetup === "other") {
|
|
2606
|
+
lines.push(import_picocolors.default.dim("├─ opencode.json"));
|
|
2607
|
+
}
|
|
2425
2608
|
}
|
|
2426
2609
|
lines.push(import_picocolors.default.dim("├─ .gitignore"));
|
|
2427
2610
|
lines.push(import_picocolors.default.dim("└─ README.md"));
|
|
@@ -2449,6 +2632,7 @@ function showReview(config) {
|
|
|
2449
2632
|
`${border} ${import_picocolors.default.dim(padLabel("rgl:"))} ${formatToggle(config.useRgl)}`,
|
|
2450
2633
|
`${border} ${import_picocolors.default.dim(padLabel("Rockide:"))} ${formatToggle(config.installRockide)}`,
|
|
2451
2634
|
`${border} ${import_picocolors.default.dim(padLabel("AI Setup:"))} ${formatValue(getAiSetupLabel(config.aiSetup))}`,
|
|
2635
|
+
...config.aiSetup !== "none" ? [`${border} ${import_picocolors.default.dim(padLabel("Ponytail:"))} ${formatToggle(config.installPonytail)}`] : [],
|
|
2452
2636
|
"",
|
|
2453
2637
|
`${border} ${teal(import_picocolors.default.bold("Planned structure"))}`,
|
|
2454
2638
|
...showFolderTree(config).split(`
|
|
@@ -2483,6 +2667,16 @@ function showPostGeneration(config) {
|
|
|
2483
2667
|
usefulCommands.push("");
|
|
2484
2668
|
usefulCommands.push(`${teal(import_picocolors.default.bold("Note"))} ${import_picocolors.default.dim(".mcp.json is ready to use — the configured MCP servers need no API keys.")}`);
|
|
2485
2669
|
}
|
|
2670
|
+
if (config.installPonytail) {
|
|
2671
|
+
usefulCommands.push("");
|
|
2672
|
+
if (config.aiSetup === "claude") {
|
|
2673
|
+
usefulCommands.push(`${teal(import_picocolors.default.bold("Ponytail"))} ${import_picocolors.default.dim("Run inside Claude Code:")}`);
|
|
2674
|
+
usefulCommands.push(` ${import_picocolors.default.cyan("/plugin marketplace add DietrichGebert/ponytail")}`);
|
|
2675
|
+
usefulCommands.push(` ${import_picocolors.default.cyan("/plugin install ponytail@ponytail")}`);
|
|
2676
|
+
} else {
|
|
2677
|
+
usefulCommands.push(`${teal(import_picocolors.default.bold("Ponytail"))} ${import_picocolors.default.dim("Added to opencode.json — installs on next opencode run.")}`);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2486
2680
|
O2.message([...nextSteps, "", ...usefulCommands].join(`
|
|
2487
2681
|
`), {
|
|
2488
2682
|
symbol: teal("▲")
|
|
@@ -2655,7 +2849,7 @@ async function runWizard() {
|
|
|
2655
2849
|
message: import_picocolors2.default.bold("AI setup"),
|
|
2656
2850
|
initialValue: "none",
|
|
2657
2851
|
options: [
|
|
2658
|
-
{ value: "none", label: "None", hint: "
|
|
2852
|
+
{ value: "none", label: "None", hint: "No AI configuration setup" },
|
|
2659
2853
|
{ value: "claude", label: "Claude", hint: "generate CLAUDE.md and .mcp.json" },
|
|
2660
2854
|
{ value: "other", label: "Other", hint: "generate AGENTS.md and .mcp.json" }
|
|
2661
2855
|
]
|
|
@@ -2663,6 +2857,17 @@ async function runWizard() {
|
|
|
2663
2857
|
if (q(aiSetup)) {
|
|
2664
2858
|
return abortWizard();
|
|
2665
2859
|
}
|
|
2860
|
+
let installPonytail = false;
|
|
2861
|
+
if (aiSetup !== "none") {
|
|
2862
|
+
const ponytailChoice = await ot2({
|
|
2863
|
+
message: `${import_picocolors2.default.bold("Install ponytail plugin?")} ${import_picocolors2.default.dim("Makes your AI agent think like the laziest senior dev in the room. The best code is the code you never wrote. https://github.com/DietrichGebert/ponytail")}`,
|
|
2864
|
+
initialValue: true
|
|
2865
|
+
});
|
|
2866
|
+
if (q(ponytailChoice)) {
|
|
2867
|
+
return abortWizard();
|
|
2868
|
+
}
|
|
2869
|
+
installPonytail = ponytailChoice;
|
|
2870
|
+
}
|
|
2666
2871
|
const config = {
|
|
2667
2872
|
projectName: trimmedProjectName,
|
|
2668
2873
|
author: author.trim(),
|
|
@@ -2674,7 +2879,8 @@ async function runWizard() {
|
|
|
2674
2879
|
useMarketplaceStructure,
|
|
2675
2880
|
useRgl: scripting === "none" ? false : useRgl,
|
|
2676
2881
|
aiSetup,
|
|
2677
|
-
installRockide: installRockide2
|
|
2882
|
+
installRockide: installRockide2,
|
|
2883
|
+
installPonytail
|
|
2678
2884
|
};
|
|
2679
2885
|
showReview(config);
|
|
2680
2886
|
const shouldGenerate = await ot2({
|
|
@@ -2703,7 +2909,7 @@ var generation = fe();
|
|
|
2703
2909
|
generation.start(teal3("Generating your Bedrock addon scaffold..."));
|
|
2704
2910
|
await generateProject(config);
|
|
2705
2911
|
if (config.aiSetup !== "none") {
|
|
2706
|
-
await setupAi(config.destination, config.aiSetup);
|
|
2912
|
+
await setupAi(config.destination, config.aiSetup, config.installPonytail);
|
|
2707
2913
|
}
|
|
2708
2914
|
await runProvisioning(config);
|
|
2709
2915
|
generation.stop(import_picocolors3.default.green("Spawnpack scaffold generated."));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spawnpack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Minecraft Bedrock addon project generator — scaffold your BP+RP in seconds",
|
|
5
5
|
"author": "veedy-dev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"dist/spawnpack.js",
|
|
14
14
|
"templates/CLAUDE.md",
|
|
15
15
|
"templates/AGENTS.md",
|
|
16
|
+
"templates/filters/typegen/main.js",
|
|
16
17
|
"README.md",
|
|
17
18
|
"LICENSE",
|
|
18
19
|
"package.json"
|
package/templates/AGENTS.md
CHANGED
|
@@ -246,27 +246,37 @@ PLAN:
|
|
|
246
246
|
ALWAYS use Exa to search the latest Minecraft Script API documentation before implementing. APIs change every major update — never trust memory.
|
|
247
247
|
</guideline>
|
|
248
248
|
|
|
249
|
-
<guideline name="
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
249
|
+
<guideline name="guard_first_error_handling" priority="critical">
|
|
250
|
+
Prefer **guard clauses** for predictable invalid state: missing values, unloaded or invalid entities, optional components, permissions, and known preconditions. `throw`, `try`, and `catch` are allowed when they are truly necessary: APIs can fail despite correct guards, you are crossing IO/persistence/parsing/async boundaries, or you need cleanup, recovery, or clearer error context.
|
|
251
|
+
|
|
252
|
+
Keep `catch` blocks narrow and purposeful. Do not use `try-catch` as normal control flow, to hide mistakes, or where a simple guard makes the failure impossible.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// ✅ CORRECT: Use guard clauses
|
|
256
|
+
function processBlock(block: Block | undefined): void {
|
|
255
257
|
if (!block) return;
|
|
256
258
|
if (!block.isValid) return;
|
|
257
259
|
const permutation = block.permutation;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
//
|
|
261
|
-
function
|
|
262
|
-
try {
|
|
263
|
-
|
|
264
|
-
} catch (
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ✅ CORRECT: Use try-catch only at a real failure boundary
|
|
263
|
+
function loadSavedConfig(rawConfig: string): AddonConfig {
|
|
264
|
+
try {
|
|
265
|
+
return JSON.parse(rawConfig) as AddonConfig;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new Error("Saved addon config is not valid JSON", { cause: error });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ❌ WRONG: Catching instead of checking known state
|
|
272
|
+
function processBlockBad(block: Block | undefined): void {
|
|
273
|
+
try {
|
|
274
|
+
const permutation = block!.permutation;
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
</guideline>
|
|
270
280
|
|
|
271
281
|
<guideline name="typescript_verification" priority="critical">
|
|
272
282
|
ALWAYS run `tsc --noEmit` after implementing. Ensure **zero warnings, zero errors, and no unused variables/imports**.
|
|
@@ -395,7 +405,7 @@ ALWAYS use Minecraft Bedrock Edition JSON Schemas for validation of BP/RP JSON f
|
|
|
395
405
|
- Use explicit return types on functions
|
|
396
406
|
- Use `readonly` for immutable properties
|
|
397
407
|
- Prefer interfaces over type aliases for object shapes
|
|
398
|
-
- Use `undefined` checks
|
|
408
|
+
- Use `undefined` checks and guard clauses before reaching for try-catch
|
|
399
409
|
</best_practices>
|
|
400
410
|
|
|
401
411
|
<best_practices name="script_api">
|
|
@@ -598,7 +608,7 @@ POTENTIAL CONCERNS:
|
|
|
598
608
|
13. Editing files from stale context without re-reading first
|
|
599
609
|
14. Duplicating state instead of fixing the real problem
|
|
600
610
|
15. Writing library/framework code from memory without searching Exa for current docs first
|
|
601
|
-
16. Using try-catch in Minecraft Script API code
|
|
611
|
+
16. Using broad try-catch in Minecraft Script API code where guard clauses would be clearer
|
|
602
612
|
17. Creating custom math functions when @minecraft/math has them available
|
|
603
613
|
18. Using raw strings instead of @minecraft/vanilla-data typed identifiers
|
|
604
614
|
19. Writing BP/RP JSON files without schema validation
|
|
@@ -614,7 +624,7 @@ You have unlimited stamina. The human does not. Use your persistence wisely —
|
|
|
614
624
|
1. **EXA FOR DOCS** — Always search library/framework docs via Exa before implementing
|
|
615
625
|
2. **VERIFY BEFORE DONE** — Type-check, lint, test before claiming success
|
|
616
626
|
3. **PLAN BEFORE BUILD** — Spec and approval before implementation
|
|
617
|
-
4. **GUARD CLAUSES** —
|
|
627
|
+
4. **GUARD CLAUSES FIRST** — Prefer guards; use throw/try-catch only at real failure boundaries
|
|
618
628
|
5. **VANILLA-DATA** — Use typed identifiers from @minecraft/vanilla-data
|
|
619
629
|
6. **MINECRAFT-MATH** — Use @minecraft/math library for math operations
|
|
620
630
|
7. **JSON SCHEMAS** — Validate BP/RP JSONs with Rockide/schemas
|
|
@@ -625,7 +635,7 @@ You have unlimited stamina. The human does not. Use your persistence wisely —
|
|
|
625
635
|
- ❌ Said "Done!" without running type-check/lint/tests? → Violated forced verification rule
|
|
626
636
|
- ❌ Edited a file from memory after 10+ messages without re-reading? → Violated context decay rule
|
|
627
637
|
- ❌ Started building without plan approval on a non-trivial task? → Violated plan-build separation
|
|
628
|
-
- ❌ Used try-catch
|
|
638
|
+
- ❌ Used broad try-catch where guards would work? → Violated guard-first error handling
|
|
629
639
|
- ❌ Made changes without running `tsc --noEmit`? → Violated TypeScript verification
|
|
630
640
|
- ❌ Created custom math function without checking @minecraft/math? → Violated library-first rule
|
|
631
641
|
- ❌ Used raw string like `"inventory"` or `"minecraft:wolf"`? → Violated vanilla-data rule
|
package/templates/CLAUDE.md
CHANGED
|
@@ -246,27 +246,37 @@ PLAN:
|
|
|
246
246
|
ALWAYS use Exa to search the latest Minecraft Script API documentation before implementing. APIs change every major update — never trust memory.
|
|
247
247
|
</guideline>
|
|
248
248
|
|
|
249
|
-
<guideline name="
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
249
|
+
<guideline name="guard_first_error_handling" priority="critical">
|
|
250
|
+
Prefer **guard clauses** for predictable invalid state: missing values, unloaded or invalid entities, optional components, permissions, and known preconditions. `throw`, `try`, and `catch` are allowed when they are truly necessary: APIs can fail despite correct guards, you are crossing IO/persistence/parsing/async boundaries, or you need cleanup, recovery, or clearer error context.
|
|
251
|
+
|
|
252
|
+
Keep `catch` blocks narrow and purposeful. Do not use `try-catch` as normal control flow, to hide mistakes, or where a simple guard makes the failure impossible.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// ✅ CORRECT: Use guard clauses
|
|
256
|
+
function processBlock(block: Block | undefined): void {
|
|
255
257
|
if (!block) return;
|
|
256
258
|
if (!block.isValid) return;
|
|
257
259
|
const permutation = block.permutation;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
//
|
|
261
|
-
function
|
|
262
|
-
try {
|
|
263
|
-
|
|
264
|
-
} catch (
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ✅ CORRECT: Use try-catch only at a real failure boundary
|
|
263
|
+
function loadSavedConfig(rawConfig: string): AddonConfig {
|
|
264
|
+
try {
|
|
265
|
+
return JSON.parse(rawConfig) as AddonConfig;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new Error("Saved addon config is not valid JSON", { cause: error });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ❌ WRONG: Catching instead of checking known state
|
|
272
|
+
function processBlockBad(block: Block | undefined): void {
|
|
273
|
+
try {
|
|
274
|
+
const permutation = block!.permutation;
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
</guideline>
|
|
270
280
|
|
|
271
281
|
<guideline name="typescript_verification" priority="critical">
|
|
272
282
|
ALWAYS run `tsc --noEmit` after implementing. Ensure **zero warnings, zero errors, and no unused variables/imports**.
|
|
@@ -395,7 +405,7 @@ ALWAYS use Minecraft Bedrock Edition JSON Schemas for validation of BP/RP JSON f
|
|
|
395
405
|
- Use explicit return types on functions
|
|
396
406
|
- Use `readonly` for immutable properties
|
|
397
407
|
- Prefer interfaces over type aliases for object shapes
|
|
398
|
-
- Use `undefined` checks
|
|
408
|
+
- Use `undefined` checks and guard clauses before reaching for try-catch
|
|
399
409
|
</best_practices>
|
|
400
410
|
|
|
401
411
|
<best_practices name="script_api">
|
|
@@ -598,7 +608,7 @@ POTENTIAL CONCERNS:
|
|
|
598
608
|
13. Editing files from stale context without re-reading first
|
|
599
609
|
14. Duplicating state instead of fixing the real problem
|
|
600
610
|
15. Writing library/framework code from memory without searching Exa for current docs first
|
|
601
|
-
16. Using try-catch in Minecraft Script API code
|
|
611
|
+
16. Using broad try-catch in Minecraft Script API code where guard clauses would be clearer
|
|
602
612
|
17. Creating custom math functions when @minecraft/math has them available
|
|
603
613
|
18. Using raw strings instead of @minecraft/vanilla-data typed identifiers
|
|
604
614
|
19. Writing BP/RP JSON files without schema validation
|
|
@@ -614,7 +624,7 @@ You have unlimited stamina. The human does not. Use your persistence wisely —
|
|
|
614
624
|
1. **EXA FOR DOCS** — Always search library/framework docs via Exa before implementing
|
|
615
625
|
2. **VERIFY BEFORE DONE** — Type-check, lint, test before claiming success
|
|
616
626
|
3. **PLAN BEFORE BUILD** — Spec and approval before implementation
|
|
617
|
-
4. **GUARD CLAUSES** —
|
|
627
|
+
4. **GUARD CLAUSES FIRST** — Prefer guards; use throw/try-catch only at real failure boundaries
|
|
618
628
|
5. **VANILLA-DATA** — Use typed identifiers from @minecraft/vanilla-data
|
|
619
629
|
6. **MINECRAFT-MATH** — Use @minecraft/math library for math operations
|
|
620
630
|
7. **JSON SCHEMAS** — Validate BP/RP JSONs with Rockide/schemas
|
|
@@ -625,7 +635,7 @@ You have unlimited stamina. The human does not. Use your persistence wisely —
|
|
|
625
635
|
- ❌ Said "Done!" without running type-check/lint/tests? → Violated forced verification rule
|
|
626
636
|
- ❌ Edited a file from memory after 10+ messages without re-reading? → Violated context decay rule
|
|
627
637
|
- ❌ Started building without plan approval on a non-trivial task? → Violated plan-build separation
|
|
628
|
-
- ❌ Used try-catch
|
|
638
|
+
- ❌ Used broad try-catch where guards would work? → Violated guard-first error handling
|
|
629
639
|
- ❌ Made changes without running `tsc --noEmit`? → Violated TypeScript verification
|
|
630
640
|
- ❌ Created custom math function without checking @minecraft/math? → Violated library-first rule
|
|
631
641
|
- ❌ Used raw string like `"inventory"` or `"minecraft:wolf"`? → Violated vanilla-data rule
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { pascalCase } from "es-toolkit";
|
|
2
|
+
import * as JSONC from "jsonc-parser";
|
|
3
|
+
import { glob, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { matchesGlob } from "node:path";
|
|
5
|
+
|
|
6
|
+
const entries = [
|
|
7
|
+
{
|
|
8
|
+
type: "BlockId",
|
|
9
|
+
pattern: "BP/blocks/**/*.json",
|
|
10
|
+
jsonPath: ["minecraft:block", "description", "identifier"],
|
|
11
|
+
identifiers: [],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
type: "EntityId",
|
|
15
|
+
pattern: "BP/entities/**/*.json",
|
|
16
|
+
jsonPath: ["minecraft:entity", "description", "identifier"],
|
|
17
|
+
identifiers: [],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: "ItemId",
|
|
21
|
+
pattern: "BP/items/**/*.json",
|
|
22
|
+
jsonPath: ["minecraft:item", "description", "identifier"],
|
|
23
|
+
identifiers: [],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const dir of ["BP", "RP"]) {
|
|
28
|
+
for await (const path of glob(`${dir}/**/*.json`)) {
|
|
29
|
+
const entry = entries.find((f) => matchesGlob(path, f.pattern));
|
|
30
|
+
if (!entry) continue;
|
|
31
|
+
|
|
32
|
+
let found = false;
|
|
33
|
+
const text = await readFile(path, "utf8");
|
|
34
|
+
JSONC.visit(text, {
|
|
35
|
+
onArrayBegin() {
|
|
36
|
+
return !found;
|
|
37
|
+
},
|
|
38
|
+
onObjectBegin() {
|
|
39
|
+
return !found;
|
|
40
|
+
},
|
|
41
|
+
onLiteralValue(value, _offset, _length, _startLine, _startCharacter, pathSupplier) {
|
|
42
|
+
if (typeof value !== "string") return;
|
|
43
|
+
if (!pathSupplier().every((segment, i) => segment === entry.jsonPath[i])) return;
|
|
44
|
+
|
|
45
|
+
found = true;
|
|
46
|
+
entry.identifiers.push(value);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lines = ["// Auto-generated file. DO NOT EDIT MANUALLY!\n"];
|
|
53
|
+
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (entry.identifiers.length === 0) continue;
|
|
56
|
+
|
|
57
|
+
lines.push(`export const enum ${entry.type} {`);
|
|
58
|
+
for (const id of entry.identifiers) {
|
|
59
|
+
const colIndex = id.indexOf(":");
|
|
60
|
+
if (colIndex === -1) {
|
|
61
|
+
console.warn(`Invalid identifier: ${id}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const name = id.slice(colIndex + 1).trim();
|
|
65
|
+
if (name === "") {
|
|
66
|
+
console.warn(`Invalid identifier: ${id}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
lines.push(`\t${pascalCase(name)} = "${id}",`);
|
|
70
|
+
}
|
|
71
|
+
lines.push(`}\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const res = lines.join("\n");
|
|
75
|
+
|
|
76
|
+
const ROOT_DIR = process.env.ROOT_DIR;
|
|
77
|
+
|
|
78
|
+
const current = await readFile(`${ROOT_DIR}/data/scripts/generated_types.ts`, "utf8")
|
|
79
|
+
.catch(() => null);
|
|
80
|
+
|
|
81
|
+
if (current !== res) {
|
|
82
|
+
await Promise.all([
|
|
83
|
+
writeFile(`${ROOT_DIR}/data/scripts/generated_types.ts`, res),
|
|
84
|
+
writeFile("./data/scripts/generated_types.ts", res),
|
|
85
|
+
]);
|
|
86
|
+
}
|