@vttforge/cli 0.0.0 → 0.5.0
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/CHANGELOG.md +350 -0
- package/LICENSE +21 -0
- package/README.md +33 -5
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +201 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/index.d.mts +496 -0
- package/dist/index.mjs +2 -0
- package/dist/src-CWhUNM__.mjs +2443 -0
- package/dist/src-CWhUNM__.mjs.map +1 -0
- package/package.json +62 -9
- package/templates/module-js/.github/workflows/release.yml +92 -0
- package/templates/module-js/README.md +57 -0
- package/templates/module-js/_gitignore +12 -0
- package/templates/module-js/lang/en.json +11 -0
- package/templates/module-js/module.json +20 -0
- package/templates/module-js/package.json +24 -0
- package/templates/module-js/scripts/main.mjs +65 -0
- package/templates/module-js/styles/main.css +16 -0
- package/templates/module-js/vite.config.mjs +13 -0
- package/templates/module-ts/.github/workflows/release.yml +92 -0
- package/templates/module-ts/README.md +57 -0
- package/templates/module-ts/_gitignore +12 -0
- package/templates/module-ts/lang/en.json +11 -0
- package/templates/module-ts/module.json +20 -0
- package/templates/module-ts/package.json +26 -0
- package/templates/module-ts/scripts/foundry-globals.ts +23 -0
- package/templates/module-ts/scripts/main.ts +71 -0
- package/templates/module-ts/styles/main.css +16 -0
- package/templates/module-ts/tsconfig.json +18 -0
- package/templates/module-ts/vite.config.mjs +13 -0
- package/templates/system-js/.github/workflows/release.yml +92 -0
- package/templates/system-js/README.md +57 -0
- package/templates/system-js/_gitignore +12 -0
- package/templates/system-js/lang/en.json +55 -0
- package/templates/system-js/package.json +25 -0
- package/templates/system-js/scripts/data/character-data.mjs +79 -0
- package/templates/system-js/scripts/data/gear-data.mjs +37 -0
- package/templates/system-js/scripts/main.mjs +79 -0
- package/templates/system-js/scripts/migrations.mjs +43 -0
- package/templates/system-js/scripts/sheets/character-sheet.mjs +164 -0
- package/templates/system-js/scripts/sheets/gear-sheet.mjs +65 -0
- package/templates/system-js/styles/main.css +495 -0
- package/templates/system-js/system.json +44 -0
- package/templates/system-js/template.json +8 -0
- package/templates/system-js/templates/actor/character-sheet.hbs +168 -0
- package/templates/system-js/templates/item/gear-sheet.hbs +54 -0
- package/templates/system-js/vite.config.mjs +16 -0
- package/templates/system-ts/.github/workflows/release.yml +92 -0
- package/templates/system-ts/README.md +57 -0
- package/templates/system-ts/_gitignore +12 -0
- package/templates/system-ts/lang/en.json +55 -0
- package/templates/system-ts/package.json +27 -0
- package/templates/system-ts/scripts/data/character-data.ts +81 -0
- package/templates/system-ts/scripts/data/gear-data.ts +37 -0
- package/templates/system-ts/scripts/foundry-globals.ts +33 -0
- package/templates/system-ts/scripts/main.ts +82 -0
- package/templates/system-ts/scripts/migrations.ts +43 -0
- package/templates/system-ts/scripts/sheets/character-sheet.ts +202 -0
- package/templates/system-ts/scripts/sheets/gear-sheet.ts +68 -0
- package/templates/system-ts/styles/main.css +495 -0
- package/templates/system-ts/system.json +44 -0
- package/templates/system-ts/template.json +8 -0
- package/templates/system-ts/templates/actor/character-sheet.hbs +168 -0
- package/templates/system-ts/templates/item/gear-sheet.hbs +54 -0
- package/templates/system-ts/tsconfig.json +18 -0
- package/templates/system-ts/vite.config.mjs +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src-CWhUNM__.mjs","names":["MANIFEST_FILES"],"sources":["../src/audit/manifest-rules.ts","../src/audit/source-rules.ts","../src/audit/types.ts","../src/audit/index.ts","../src/audit/reporter.ts","../src/commands/audit.ts","../src/manifest.ts","../src/package-manager.ts","../src/vite-runner.ts","../src/zip.ts","../src/commands/build.ts","../src/symlink.ts","../src/dev-module-install.ts","../src/dev-server.ts","../src/dev-watcher.ts","../src/foundry-data-dir.ts","../src/commands/dev.ts","../src/git.ts","../src/scaffold.ts","../src/commands/init.ts","../src/index.ts"],"sourcesContent":["/**\n * Manifest-scope audit rules.\n *\n * These rules operate on the parsed contents of `system.json` and/or\n * `module.json` at the project root. They cover three v13 manifest\n * footguns from the VTTForge audit catalog:\n *\n * VTTF-AUDIT-001 (HIGH) — flags.hotReload shape\n * VTTF-AUDIT-002 (MEDIUM) — deprecated gridDistance/gridUnits\n * VTTF-AUDIT-003 (LOW) — styles array of strings (v12 shape)\n *\n * Each rule emits zero or more `RuleResult`s. Line numbers are looked up\n * cheaply by scanning the raw JSON for the offending key — accurate\n * enough for navigation, no AST dependency.\n */\n\nimport { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { RuleResult } from './types.js';\n\nconst MANIFEST_FILES = ['system.json', 'module.json'] as const;\n\ninterface LoadedManifest {\n path: string;\n raw: string;\n parsed: Record<string, unknown>;\n}\n\n/**\n * Load the manifest(s) at the project root. Returns at most two entries\n * (system + module), skipping anything that can't be parsed as a JSON\n * object. We tolerate parse errors here because that's a separate failure\n * mode the user will hit when they actually run `vite build`; the audit\n * shouldn't double-report it.\n */\nasync function loadManifests(cwd: string): Promise<LoadedManifest[]> {\n const out: LoadedManifest[] = [];\n for (const file of MANIFEST_FILES) {\n const path = join(cwd, file);\n if (!existsSync(path)) continue;\n let raw: string;\n try {\n raw = await readFile(path, 'utf8');\n } catch {\n continue;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n continue;\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n out.push({ path, raw, parsed: parsed as Record<string, unknown> });\n }\n }\n return out;\n}\n\n/**\n * Find the 1-based line number of the first `\"key\":` occurrence in JSON\n * source. Good enough for navigation — JSON keys are usually unique at\n * the level we report on, and even when nested duplicates exist the\n * first occurrence points at the right region of the file.\n */\nfunction findKeyLine(raw: string, key: string): number | undefined {\n const needle = `\"${key}\"`;\n const idx = raw.indexOf(needle);\n if (idx < 0) return undefined;\n let line = 1;\n for (let i = 0; i < idx; i += 1) {\n if (raw[i] === '\\n') line += 1;\n }\n return line;\n}\n\n/**\n * VTTF-AUDIT-001 (HIGH) — flags.hotReload shape.\n *\n * v12 accepted `\"hotReload\": [\"css\", \"hbs\", ...]` (array). v13 expects\n * `\"hotReload\": { \"extensions\": [...], \"paths\": [...] }` (object at the\n * root of `flags`, NOT nested under a package id). With the array shape\n * the server's chokidar watcher reads `.extensions` and gets undefined,\n * silently disabling HMR for the package.\n */\nfunction ruleHotReload(manifest: LoadedManifest): RuleResult[] {\n const flags = manifest.parsed.flags;\n if (!flags || typeof flags !== 'object' || Array.isArray(flags)) return [];\n const hr = (flags as Record<string, unknown>).hotReload;\n if (hr === undefined) return [];\n\n const line = findKeyLine(manifest.raw, 'hotReload');\n if (Array.isArray(hr)) {\n return [\n {\n ruleId: 'VTTF-AUDIT-001',\n title: 'flags.hotReload uses the deprecated v12 array shape',\n severity: 'HIGH',\n filePath: manifest.path,\n line,\n message:\n 'flags.hotReload is an array; v13 expects an object `{ \"extensions\": [...], \"paths\": [...] }`. With the array shape Foundry silently disables HMR for this package.',\n remediation:\n 'Replace the array with an object: `\"hotReload\": { \"extensions\": [\"css\", \"hbs\", \"json\"], \"paths\": [\"styles\", \"templates\", \"lang\"] }`.',\n },\n ];\n }\n // `typeof null === 'object'` would slip past a naive check, so the\n // null / scalar / array branches are all rejected explicitly before\n // we trust hr as an object.\n if (hr === null || typeof hr !== 'object') {\n return [\n {\n ruleId: 'VTTF-AUDIT-001',\n title: 'flags.hotReload has an invalid shape',\n severity: 'HIGH',\n filePath: manifest.path,\n line,\n message: 'flags.hotReload must be an object with `extensions` and `paths` arrays in v13.',\n remediation: 'Replace with `\"hotReload\": { \"extensions\": [...], \"paths\": [...] }`.',\n },\n ];\n }\n // Object shape — `extensions` is mandatory (Foundry has no default for\n // which extensions to watch), but `paths` is optional: when omitted,\n // Foundry watches the entire package root. Don't penalise users who\n // legitimately want the broader watch scope.\n const hrObj = hr as Record<string, unknown>;\n if (!Array.isArray(hrObj.extensions)) {\n return [\n {\n ruleId: 'VTTF-AUDIT-001',\n title: 'flags.hotReload is missing the required `extensions` key',\n severity: 'HIGH',\n filePath: manifest.path,\n line,\n message:\n 'flags.hotReload must declare an `extensions` array — Foundry has no default and the watcher silently does nothing without it.',\n remediation:\n 'Add `extensions`: `\"hotReload\": { \"extensions\": [\"css\", \"hbs\", \"json\"], \"paths\": [\"styles\", \"templates\", \"lang\"] }`. `paths` is optional (defaults to the package root) but recommended.',\n },\n ];\n }\n return [];\n}\n\n/**\n * VTTF-AUDIT-002 (MEDIUM) — deprecated top-level grid fields.\n *\n * v12 used flat `gridDistance` + `gridUnits`. v13 wants\n * `grid: { type, distance, units, diagonals }`. The legacy keys still\n * work today (Foundry auto-migrates and warns) but will be removed in\n * v14 — flag them now so the project is forward-compatible.\n */\nfunction ruleGridShape(manifest: LoadedManifest): RuleResult[] {\n const hasLegacyDistance = 'gridDistance' in manifest.parsed;\n const hasLegacyUnits = 'gridUnits' in manifest.parsed;\n if (!hasLegacyDistance && !hasLegacyUnits) return [];\n\n const offendingKey = hasLegacyDistance ? 'gridDistance' : 'gridUnits';\n return [\n {\n ruleId: 'VTTF-AUDIT-002',\n title: 'Top-level gridDistance / gridUnits are deprecated v12 fields',\n severity: 'MEDIUM',\n filePath: manifest.path,\n line: findKeyLine(manifest.raw, offendingKey),\n message:\n 'gridDistance and gridUnits at the manifest root are the v12 shape. Foundry v13 expects a structured `grid` object and emits a deprecation warning on load; the fields will be removed in v14.',\n remediation:\n 'Replace with `\"grid\": { \"type\": 1, \"distance\": 5, \"units\": \"ft\", \"diagonals\": 0 }` (adjust values to your system).',\n },\n ];\n}\n\n/**\n * VTTF-AUDIT-003 (LOW) — styles array of strings.\n *\n * v12 took `styles: [\"styles/foo.css\"]`. v13 expects `styles: [{ src,\n * layer? }]` so cascade-layer ordering can be declared in the manifest.\n * The string form still works (Foundry auto-migrates) but emits a\n * deprecation warning and loses the ability to control layer placement.\n */\nfunction ruleStylesShape(manifest: LoadedManifest): RuleResult[] {\n const styles = manifest.parsed.styles;\n if (!Array.isArray(styles) || styles.length === 0) return [];\n\n // The whole array is uniform in the v12 shape — entries are strings.\n // Mixed arrays (some string, some object) also fail v13 expectations.\n const hasString = styles.some((entry) => typeof entry === 'string');\n if (!hasString) return [];\n\n return [\n {\n ruleId: 'VTTF-AUDIT-003',\n title: 'styles uses the deprecated v12 string-array shape',\n severity: 'LOW',\n filePath: manifest.path,\n line: findKeyLine(manifest.raw, 'styles'),\n message:\n 'styles entries should be objects of the form `{ \"src\": \"path/to.css\", \"layer\": \"optional-layer\" }` in v13. String entries auto-migrate today but the conversion drops your control over cascade-layer placement.',\n remediation:\n 'Replace each string with `{ \"src\": \"<that-path>\" }`. Add a `\"layer\"` field per entry where you want explicit cascade ordering.',\n },\n ];\n}\n\n/**\n * Entry point: run every manifest rule against every manifest at the\n * project root.\n */\nexport async function runManifestRules(cwd: string): Promise<RuleResult[]> {\n const manifests = await loadManifests(cwd);\n const results: RuleResult[] = [];\n for (const manifest of manifests) {\n results.push(...ruleHotReload(manifest));\n results.push(...ruleGridShape(manifest));\n results.push(...ruleStylesShape(manifest));\n }\n return results;\n}\n","/**\n * Source-tree audit rules.\n *\n * These walk `.ts` / `.tsx` / `.mjs` / `.cjs` / `.js` files under the\n * project root (excluding common build/dependency dirs) and apply regex\n * heuristics to spot four v13 footguns from the VTTForge audit catalog:\n *\n * VTTF-AUDIT-004 (MEDIUM) — HTMLField/FilePathField missing manifest declaration\n * VTTF-AUDIT-005 (MEDIUM) — extends TypeDataModel without prepareBaseData stub\n * VTTF-AUDIT-006 (LOW) — `_addDataFieldMigrations` override (wrong signature)\n * VTTF-AUDIT-007 (MEDIUM) — manifest primary/secondaryTokenAttribute not matched\n * by a `value`/`max` SchemaField in source\n *\n * Regex-based on purpose: avoids pulling in a TypeScript AST dependency\n * for what amount to four pattern checks. The trade-off is occasional\n * false negatives on heavily-formatted code; the alternative would be a\n * 30MB+ runtime dep for marginal gains.\n */\n\nimport { existsSync } from 'node:fs';\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { RuleResult } from './types.js';\n\n/** Directories we never descend into — they're not user source. */\nconst EXCLUDED_DIRS = new Set([\n 'node_modules',\n 'dist',\n 'build',\n '.git',\n '.turbo',\n '.vttforge',\n '.next',\n 'coverage',\n '.vitest-cache',\n]);\n\n// `.mts`/`.cts` are valid TS ESM/CJS source files; `.d.ts`/`.d.mts`/`.d.cts`\n// are declaration-only and skipped (no real source to scan).\nconst SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.mjs', '.cjs', '.js'] as const;\n\nfunction isSourceFile(name: string): boolean {\n if (name.endsWith('.d.ts') || name.endsWith('.d.mts') || name.endsWith('.d.cts')) {\n return false;\n }\n return SOURCE_EXTENSIONS.some((ext) => name.endsWith(ext));\n}\n\nasync function* walkSourceFiles(cwd: string): AsyncGenerator<string> {\n const entries = await readdir(cwd, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith('.') && EXCLUDED_DIRS.has(entry.name)) continue;\n if (entry.isDirectory()) {\n if (EXCLUDED_DIRS.has(entry.name)) continue;\n yield* walkSourceFiles(join(cwd, entry.name));\n } else if (entry.isFile() && isSourceFile(entry.name)) {\n yield join(cwd, entry.name);\n }\n }\n}\n\n/** Find the 1-based line number of the first occurrence of `needle`. */\nfunction lineOf(content: string, needle: string | RegExp): number | undefined {\n const idx = typeof needle === 'string' ? content.indexOf(needle) : content.search(needle);\n if (idx < 0) return undefined;\n let line = 1;\n for (let i = 0; i < idx; i += 1) {\n if (content[i] === '\\n') line += 1;\n }\n return line;\n}\n\n/** VTTF-AUDIT-005 — `extends TypeDataModel` without a `prepareBaseData(...)` method. */\nfunction rule005(filePath: string, content: string): RuleResult[] {\n // Find every direct TypeDataModel subclass with its body extents so we\n // can check each class independently. The previous file-wide check\n // would pass a whole file if ANY class in it had prepareBaseData; that\n // silently let sibling subclasses miss the hook.\n const out: RuleResult[] = [];\n const directClasses = findDirectTypeDataModelClasses(content);\n for (const cls of directClasses) {\n const body = content.slice(cls.openIdx + 1, cls.endIdx);\n if (/prepareBaseData\\s*\\(/.test(body)) continue;\n out.push({\n ruleId: 'VTTF-AUDIT-005',\n title: 'TypeDataModel subclass missing prepareBaseData stub',\n severity: 'MEDIUM',\n filePath,\n line: cls.startLine,\n message: `Class \\`${cls.className}\\` extends TypeDataModel directly but does not define prepareBaseData(). Active Effects apply between prepareBaseData and prepareDerivedData; without the hook, AE consumers see uninitialised fields.`,\n remediation:\n 'Add a no-op `prepareBaseData()` method (or extend `BaseTypeDataModel()` from `@vttforge/core`, which provides the stub).',\n });\n }\n return out;\n}\n\n/**\n * Subset of `findClassRanges` that only returns classes whose `extends`\n * names `TypeDataModel` (and not via the `BaseTypeDataModel()` factory).\n * The word boundary in `\\bTypeDataModel\\b` keeps the factory pattern\n * out, same as the lower-level helper.\n */\nfunction findDirectTypeDataModelClasses(content: string): ClassRange[] {\n const re = /class\\s+(\\w+)\\s+extends\\s+(?:[\\w.]+\\.)?\\bTypeDataModel\\b[^{]*\\{/g;\n const out: ClassRange[] = [];\n for (const match of content.matchAll(re)) {\n if (match.index === undefined) continue;\n const openIdx = match.index + match[0].length - 1;\n let depth = 0;\n let endIdx = -1;\n for (let i = openIdx; i < content.length; i += 1) {\n if (content[i] === '{') depth += 1;\n else if (content[i] === '}') {\n depth -= 1;\n if (depth === 0) {\n endIdx = i;\n break;\n }\n }\n }\n if (endIdx < 0) continue;\n let line = 1;\n for (let i = 0; i < match.index; i += 1) {\n if (content[i] === '\\n') line += 1;\n }\n out.push({ className: match[1] ?? '(anonymous)', startLine: line, openIdx, endIdx });\n }\n return out;\n}\n\n/** VTTF-AUDIT-006 — `_addDataFieldMigrations(` override on a TypeDataModel subclass. */\nfunction rule006(filePath: string, content: string): RuleResult[] {\n if (!/_addDataFieldMigrations\\s*\\(/.test(content)) return [];\n // Only flag when the file defines a TypeDataModel-derived class — direct\n // `TypeDataModel` OR the VTTForge `BaseTypeDataModel()` factory. The\n // factory case matters because templates lean on it; an override there\n // is the most likely place users would write the buggy signature.\n if (!/extends\\s+(?:[\\w.]+\\.)?(?:Base)?TypeDataModel\\b/.test(content)) return [];\n return [\n {\n ruleId: 'VTTF-AUDIT-006',\n title: 'Method name confused with Foundry migration API',\n severity: 'LOW',\n filePath,\n line: lineOf(content, /_addDataFieldMigrations\\s*\\(/),\n message:\n 'The plural `_addDataFieldMigrations` is not a real Foundry API. The canonical v13 migration hook is a static `migrateData(source)` override that calls singular `super._addDataFieldMigration(source, oldKey, newKey, apply?)` for each rename. Instance overrides named `_addDataFieldMigrations` never run.',\n remediation:\n 'Replace the override with `static migrateData(source) { super._addDataFieldMigration(source, \"oldKey\", \"newKey\"); return super.migrateData(source); }`.',\n },\n ];\n}\n\n/**\n * Track each class declaration's brace-balanced range so we can ask\n * \"which class encloses position X?\" later. Without this, rule 004 has\n * to fall back to a global declared-fields union, which silently passes\n * when two subtypes share a field name but only one declares it.\n */\ninterface ClassRange {\n className: string;\n startLine: number;\n openIdx: number;\n endIdx: number;\n}\n\nfunction findClassRanges(content: string): ClassRange[] {\n const out: ClassRange[] = [];\n const re = /class\\s+(\\w+)(?:\\s+extends\\s+[^{]+)?\\{/g;\n for (const match of content.matchAll(re)) {\n if (match.index === undefined) continue;\n const openIdx = match.index + match[0].length - 1;\n let depth = 0;\n let endIdx = -1;\n for (let i = openIdx; i < content.length; i += 1) {\n if (content[i] === '{') depth += 1;\n else if (content[i] === '}') {\n depth -= 1;\n if (depth === 0) {\n endIdx = i;\n break;\n }\n }\n }\n if (endIdx < 0) continue;\n let line = 1;\n for (let i = 0; i < match.index; i += 1) {\n if (content[i] === '\\n') line += 1;\n }\n out.push({ className: match[1] ?? '(anonymous)', startLine: line, openIdx, endIdx });\n }\n return out;\n}\n\nfunction findEnclosingClass(ranges: ClassRange[], idx: number): string | undefined {\n // Inner class wins (later in the array, since classes are flat in typical\n // Foundry code). Iterate in reverse so a nested class beats its parent.\n for (let i = ranges.length - 1; i >= 0; i -= 1) {\n const r = ranges[i];\n if (!r) continue;\n if (idx >= r.openIdx && idx <= r.endIdx) return r.className;\n }\n return undefined;\n}\n\n/**\n * Detect class→subtype registrations in three forms:\n *\n * 1. `CONFIG.<Doc>.dataModels.<subtype> = <ClassName>` (direct assignment)\n * 2. `CONFIG.<Doc>.dataModels = { <subtype>: <ClassName>, … }` (block)\n * 3. `registerSystem({ actorDataModels: { <subtype>: <ClassName>, … }, … })`\n * and the analogous `itemDataModels`, `activeEffectDataModels`, etc.\n * — VTTForge's templated entry point uses this form, so without case\n * (3) the bundled scaffolds register through an unknown surface and\n * rule 004 falls back to the looser global-union check.\n */\ninterface ConfigMapping {\n doc: string;\n subtype: string;\n className: string;\n}\n\n/**\n * One `<subtype>: <Class>` entry inside a dataModels object literal.\n *\n * The key is bare for a system (`character`) and quoted for a module\n * (`'my-module.vehicle'`), because the module form contains a dot.\n */\nconst SUBTYPE_ENTRY_RE = /(?:['\"]([^'\"]+)['\"]|(\\w+))\\s*:\\s*(\\w+)/g;\n\n/** Map a `registerSystem({ ...DataModels })` key to the document name. */\nconst REGISTER_SYSTEM_KEYS: ReadonlyArray<{ key: string; doc: string }> = [\n { key: 'actorDataModels', doc: 'Actor' },\n { key: 'itemDataModels', doc: 'Item' },\n { key: 'activeEffectDataModels', doc: 'ActiveEffect' },\n { key: 'journalEntryPageDataModels', doc: 'JournalEntryPage' },\n { key: 'regionBehaviorDataModels', doc: 'RegionBehavior' },\n];\n\nfunction findConfigMappings(content: string): ConfigMapping[] {\n const out: ConfigMapping[] = [];\n\n // (1) Direct CONFIG.<Doc>.dataModels.<subtype> = <Class>\n const direct = /CONFIG\\.(\\w+)\\.dataModels\\.(\\w+)\\s*=\\s*(\\w+)/g;\n for (const m of content.matchAll(direct)) {\n out.push({ doc: m[1] ?? '', subtype: m[2] ?? '', className: m[3] ?? '' });\n }\n\n // (1b) Bracket CONFIG.<Doc>.dataModels['<subtype>'] = <Class>\n //\n // Modules must use this form: Foundry registers their subtypes as\n // `<moduleId>.<type>`, and a dot in the key rules out property access.\n // Without this case module registrations are invisible, every module\n // field falls through to the looser global check, and a genuinely\n // undeclared path can hide behind a same-named one declared elsewhere.\n const bracket = /CONFIG\\.(\\w+)\\.dataModels\\[\\s*['\"]([^'\"]+)['\"]\\s*\\]\\s*=\\s*(\\w+)/g;\n for (const m of content.matchAll(bracket)) {\n out.push({ doc: m[1] ?? '', subtype: m[2] ?? '', className: m[3] ?? '' });\n }\n\n // (2) Block CONFIG.<Doc>.dataModels = { <subtype>: <Class>, … }\n const block = /CONFIG\\.(\\w+)\\.dataModels\\s*=\\s*\\{([\\s\\S]*?)\\}/g;\n for (const m of content.matchAll(block)) {\n const doc = m[1] ?? '';\n const body = m[2] ?? '';\n for (const e of body.matchAll(SUBTYPE_ENTRY_RE)) {\n out.push({ doc, subtype: e[1] ?? e[2] ?? '', className: e[3] ?? '' });\n }\n }\n\n // (3) registerSystem({ actorDataModels: { ... }, itemDataModels: { ... }, … })\n // Find every `<KeyName>: {` block within the file body, then grab the\n // pairs inside. We use balanced-brace extraction so a nested SchemaField\n // initializer doesn't truncate the body — but `dataModels` blocks\n // typically only contain simple `<subtype>: <ClassName>` entries.\n for (const { key, doc } of REGISTER_SYSTEM_KEYS) {\n const re = new RegExp(String.raw`\\b${key}\\s*:\\s*\\{`, 'g');\n for (const m of content.matchAll(re)) {\n if (m.index === undefined) continue;\n const openIdx = m.index + m[0].length - 1;\n let depth = 0;\n let endIdx = -1;\n for (let i = openIdx; i < content.length; i += 1) {\n if (content[i] === '{') depth += 1;\n else if (content[i] === '}') {\n depth -= 1;\n if (depth === 0) {\n endIdx = i;\n break;\n }\n }\n }\n if (endIdx < 0) continue;\n const body = content.slice(openIdx + 1, endIdx);\n for (const pair of body.matchAll(SUBTYPE_ENTRY_RE)) {\n out.push({ doc, subtype: pair[1] ?? pair[2] ?? '', className: pair[3] ?? '' });\n }\n }\n }\n\n return out;\n}\n\n/**\n * Match `<name>: new (f|fields|foundry.data.fields).HTMLField(` or\n * `.FilePathField(`. We capture the field name, the field type, the\n * enclosing class, AND the dot-path through any enclosing SchemaField\n * wrappers — so rule 004 compares against the FULL manifest declaration\n * path (`profile.bio`), not just the leaf segment (`bio`).\n */\nconst RICH_FIELD_RE = /(\\w+)\\s*:\\s*new\\s+(?:[\\w.]+\\.)?(HTMLField|FilePathField)\\s*\\(/g;\n\ninterface RichFieldUsage {\n fieldName: string;\n type: 'HTMLField' | 'FilePathField';\n line: number;\n enclosingClass: string | undefined;\n /** Dot-path through SchemaField nesting (e.g. `profile.bio`, or just `bio` at top level). */\n schemaPath: string;\n}\n\nfunction findRichFields(content: string, classes: ClassRange[]): RichFieldUsage[] {\n const out: RichFieldUsage[] = [];\n for (const m of content.matchAll(RICH_FIELD_RE)) {\n if (m.index === undefined) continue;\n let line = 1;\n for (let i = 0; i < m.index; i += 1) {\n if (content[i] === '\\n') line += 1;\n }\n const fieldName = m[1] ?? '(anonymous)';\n out.push({\n fieldName,\n type: (m[2] as 'HTMLField' | 'FilePathField') ?? 'HTMLField',\n line,\n enclosingClass: findEnclosingClass(classes, m.index),\n schemaPath: buildSchemaPath(content, m.index, fieldName),\n });\n }\n return out;\n}\n\n/**\n * Walk backward from a rich-field match looking for enclosing SchemaField\n * wrappers, building the dot-path up to the outermost schema.\n *\n * For `profile: new SchemaField({ bio: new HTMLField() })`, the rich-field\n * match is `bio: new HTMLField(`; walking back, we find the enclosing `{`\n * that's preceded by `profile: new ...SchemaField(`, prepend `profile`, and\n * recurse. Top-level fields (immediate children of `defineSchema()`'s\n * returned object) recurse until the enclosing `{` has no SchemaField\n * ancestor, returning just the field name.\n */\nfunction buildSchemaPath(content: string, idx: number, fieldName: string): string {\n // Find the immediate enclosing `{` by walking backward, tracking balance.\n let depth = 0;\n let openIdx = -1;\n for (let i = idx - 1; i >= 0; i -= 1) {\n const ch = content[i];\n if (ch === '}') depth += 1;\n else if (ch === '{') {\n if (depth === 0) {\n openIdx = i;\n break;\n }\n depth -= 1;\n }\n }\n if (openIdx < 0) return fieldName;\n\n // Look at the text immediately preceding the enclosing `{` for a\n // `<key>: new <prefix>SchemaField(` pattern. The lookback window is\n // bounded so we don't accidentally cross other constructs.\n const lookbackStart = Math.max(0, openIdx - 200);\n const before = content.slice(lookbackStart, openIdx);\n const m = before.match(/(\\w+)\\s*:\\s*new\\s+(?:[\\w.]+\\.)?SchemaField\\s*\\(\\s*$/);\n if (!m) return fieldName;\n const parentName = m[1] ?? '';\n if (!parentName) return fieldName;\n // The parent's match position relative to content — recurse from there\n // so multi-level nesting (`profile.contact.email`) is captured.\n const parentMatchStart = lookbackStart + (m.index ?? 0);\n const parentPath = buildSchemaPath(content, parentMatchStart, parentName);\n return `${parentPath}.${fieldName}`;\n}\n\ninterface DeclaredFields {\n /**\n * Keyed `Doc.subtype` → declared htmlFields / filePathFields.\n * Paths are stored AFTER stripping a leading `system.` prefix (Foundry\n * convention puts TypeDataModel-driven manifests under `system.…`) so\n * we can compare against source-side paths that don't include the\n * `system.` root.\n */\n perSubtype: Map<string, { html: Set<string>; filePath: Set<string> }>;\n /**\n * The manifest's own `id`. Modules declare subtypes under a bare key\n * (`vehicle`) but Foundry registers them prefixed (`my-module.vehicle`),\n * which is the form that appears in source. Stripping this prefix is\n * what lets the two sides meet.\n */\n packageId: string | null;\n /** Union across every subtype — used as a fallback when class→subtype lookup fails. */\n globalHtml: Set<string>;\n globalFilePath: Set<string>;\n}\n\n/**\n * Foundry's manifest convention puts TypeDataModel-driven field paths\n * under the `system.` root (`system.biography`). Source-side rich-field\n * paths from `buildSchemaPath` don't include that prefix because they\n * start from `defineSchema()`'s returned object. Strip it for matching.\n */\nfunction normalizeDeclaredPath(path: string): string {\n return path.startsWith('system.') ? path.slice('system.'.length) : path;\n}\n\n/**\n * Find one subtype's declarations, accounting for the module prefix.\n *\n * A system declares `character` in the manifest and registers `character`\n * in source — the two match directly. A module declares `vehicle` but\n * Foundry registers it as `my-module.vehicle`, and that prefixed form is\n * what appears in source. Comparing the two verbatim reports every\n * correctly declared module field as missing, so fall back to the bare key\n * once the package's own prefix is stripped.\n *\n * Only this package's prefix is stripped. A subtype contributed by some\n * other module is genuinely not ours to have declared.\n */\nfunction lookupSubtype(\n declared: DeclaredFields,\n doc: string,\n subtype: string,\n): { html: Set<string>; filePath: Set<string> } | undefined {\n const direct = declared.perSubtype.get(`${doc}.${subtype}`);\n if (direct) return direct;\n const prefix = declared.packageId ? `${declared.packageId}.` : null;\n if (!prefix || !subtype.startsWith(prefix)) return undefined;\n return declared.perSubtype.get(`${doc}.${subtype.slice(prefix.length)}`);\n}\n\n/**\n * VTTF-AUDIT-004 — cross-check source HTMLField/FilePathField usages\n * against the manifest's documentTypes declarations.\n *\n * Subtype-aware: if the enclosing class is registered on\n * `CONFIG.<Doc>.dataModels.<subtype>`, the field must be declared in\n * THAT subtype's htmlFields / filePathFields. Otherwise — class\n * unregistered, registration spread across files, dynamic registration —\n * we fall back to the global union check (catches the common \"forgot to\n * declare anything\" case without false positives on advanced setups).\n */\nfunction rule004(\n filePath: string,\n content: string,\n declared: DeclaredFields,\n classToSubtypes: Map<string, ConfigMapping[]>,\n): RuleResult[] {\n const classes = findClassRanges(content);\n const usages = findRichFields(content, classes);\n const out: RuleResult[] = [];\n for (const usage of usages) {\n const declaredBucketName = usage.type === 'HTMLField' ? 'htmlFields' : 'filePathFields';\n const mappings = usage.enclosingClass ? (classToSubtypes.get(usage.enclosingClass) ?? []) : [];\n const sourcePath = usage.schemaPath;\n\n if (mappings.length > 0) {\n // Strict per-subtype check: every subtype this class is registered\n // under must declare the field's FULL source path. `profile.bio`\n // declared in `Actor.character` doesn't cover an undeclared\n // `profile.bio` in `Actor.npc`.\n const missing = mappings.filter((mapping) => {\n const decl = lookupSubtype(declared, mapping.doc, mapping.subtype);\n const bucket = usage.type === 'HTMLField' ? decl?.html : decl?.filePath;\n return !bucket?.has(sourcePath);\n });\n if (missing.length > 0) {\n const locations = missing.map((mapping) => `${mapping.doc}.${mapping.subtype}`).join(', ');\n out.push({\n ruleId: 'VTTF-AUDIT-004',\n title: `${usage.type} not declared in manifest documentTypes`,\n severity: 'MEDIUM',\n filePath,\n line: usage.line,\n message: `\\`${usage.enclosingClass}\\` declares \\`${sourcePath}\\` as ${usage.type}, but documentTypes.${locations}.${declaredBucketName} does not list this path. The Foundry server only sanitises declared paths.`,\n remediation: `Add \\`${sourcePath}\\` (or \\`system.${sourcePath}\\`) to documentTypes.${missing[0]?.doc}.${missing[0]?.subtype}.${declaredBucketName} in your manifest.`,\n });\n }\n continue;\n }\n\n // Fallback: no class→subtype mapping detected (class not registered,\n // registration via an unrecognized API, dynamic registration). Check\n // the global union — still useful, but with looser semantics. Match\n // on FULL source path so `profile.bio` declared doesn't silently\n // shadow an undeclared `journal.bio`.\n const bucket = usage.type === 'HTMLField' ? declared.globalHtml : declared.globalFilePath;\n if (bucket.has(sourcePath)) continue;\n out.push({\n ruleId: 'VTTF-AUDIT-004',\n title: `${usage.type} not declared in manifest documentTypes`,\n severity: 'MEDIUM',\n filePath,\n line: usage.line,\n message: `Schema declares \\`${sourcePath}\\` as ${usage.type}, but no documentTypes.<Doc>.<subtype>.${declaredBucketName} entry lists this path. The Foundry server only sanitises declared paths.`,\n remediation: `Add \\`${sourcePath}\\` (or \\`system.${sourcePath}\\`) to documentTypes.<Doc>.<subtype>.${declaredBucketName} in your manifest.`,\n });\n }\n return out;\n}\n\n/**\n * VTTF-AUDIT-007 — manifest's `primaryTokenAttribute` /\n * `secondaryTokenAttribute` must point at a SchemaField with `value` +\n * `max` keys. If not, Foundry's `getBarAttribute` silently degrades to\n * value-only rendering (no bar at all).\n *\n * Cross-check walks the source for every SchemaField declaration paired\n * with its full schema path (`buildSchemaPath`). The manifest path is\n * matched EXACTLY — `primaryTokenAttribute: 'health'` does not accept a\n * nested `attributes.health` SchemaField, and a nested path like\n * `attributes.hp` is resolved by finding the declaration at that exact\n * dot-path. No more \"verify manually\" branch; either the path resolves\n * or it doesn't.\n */\nasync function rule007(\n manifestPath: string | null,\n manifestRaw: string | null,\n manifestParsed: Record<string, unknown> | null,\n sourceFiles: string[],\n classToSubtypes: Map<string, ConfigMapping[]>,\n): Promise<RuleResult[]> {\n if (!manifestPath || !manifestRaw || !manifestParsed) return [];\n const actorClasses = new Set(\n [...classToSubtypes.entries()]\n .filter(([, mappings]) => mappings.some((m) => m.doc === 'Actor'))\n .map(([className]) => className),\n );\n\n const out: RuleResult[] = [];\n for (const key of ['primaryTokenAttribute', 'secondaryTokenAttribute'] as const) {\n const value = manifestParsed[key];\n if (typeof value !== 'string' || value.length === 0) continue;\n const matched = await sourceHasValueMaxSchemaAtPath(sourceFiles, value, actorClasses);\n if (!matched) {\n out.push({\n ruleId: 'VTTF-AUDIT-007',\n title: `${key} does not resolve to a {value, max} SchemaField`,\n severity: 'MEDIUM',\n filePath: manifestPath,\n line: lineOfInRaw(manifestRaw, key),\n message: `Manifest ${key} is \\`${value}\\`, but no TypeDataModel schema in the source declares a SchemaField at this path with both \\`value\\` and \\`max\\` keys. Foundry's getBarAttribute degrades silently when the structure doesn't match.`,\n remediation: `Define \\`${value}\\` as a SchemaField with \\`value\\` and \\`max\\` NumberField children in your TypeDataModel schema (matching the manifest path exactly).`,\n });\n }\n }\n return out;\n}\n\nfunction lineOfInRaw(raw: string, key: string): number | undefined {\n const idx = raw.indexOf(`\"${key}\"`);\n if (idx < 0) return undefined;\n let line = 1;\n for (let i = 0; i < idx; i += 1) {\n if (raw[i] === '\\n') line += 1;\n }\n return line;\n}\n\n/**\n * Path-aware lookup for the rule-007 cross-check. For every SchemaField\n * declaration in every source file, compute its FULL schema path via\n * `buildSchemaPath`. Match the manifest's `primaryTokenAttribute` (which\n * may be a single key like `health` or a dotted path like `attributes.hp`)\n * against the declaration's path exactly. Only declarations whose schema\n * path equals the manifest value contribute to the value+max check.\n *\n * Why exact path: an unqualified `health: new SchemaField(...)` inside\n * `attributes: new SchemaField({...})` has schema path `attributes.health`.\n * Manifest `primaryTokenAttribute: 'health'` should NOT resolve to that\n * nested declaration — Foundry walks the document path structurally and\n * would look for `health.value` at the top of `system`, not under\n * `attributes`.\n */\nasync function sourceHasValueMaxSchemaAtPath(\n sourceFiles: string[],\n targetPath: string,\n actorClasses: ReadonlySet<string>,\n): Promise<boolean> {\n for (const file of sourceFiles) {\n let content: string;\n try {\n content = await readFile(file, 'utf8');\n } catch {\n continue;\n }\n const classes = findClassRanges(content);\n for (const decl of findAllSchemaFields(content)) {\n if (decl.path !== targetPath) continue;\n // Token bars read `actor.system`, so only a schema registered as an\n // Actor data model can satisfy the manifest. An Item model that\n // happens to declare the same path does not — accepting it would\n // pass the rule on a system whose token bars are in fact broken.\n //\n // This narrowing only applies when the registrations are known.\n // With none found there is no ground truth to narrow by, so every\n // declaration counts — same fallback rule 004 takes when it cannot\n // resolve a class to its subtypes.\n if (actorClasses.size > 0) {\n const owner = classes.find((c) => decl.index > c.openIdx && decl.index < c.endIdx);\n if (!owner || !actorClasses.has(owner.className)) continue;\n }\n const topKeys = extractTopLevelKeys(decl.body);\n if (topKeys.has('value') && topKeys.has('max')) return true;\n }\n }\n return false;\n}\n\n/**\n * Find every `<name>: new ...SchemaField({...})` declaration in `content`,\n * returning each with its FULL dot-path through enclosing SchemaField\n * wrappers and the raw object-literal body. Used by rule 007 to look up\n * a SchemaField at an exact manifest path.\n */\ninterface SchemaFieldDecl {\n path: string;\n body: string;\n /** Offset of the declaration, used to find its enclosing class. */\n index: number;\n}\n\nfunction findAllSchemaFields(content: string): SchemaFieldDecl[] {\n const out: SchemaFieldDecl[] = [];\n const startRe = /(\\w+)\\s*:\\s*new\\s+(?:[\\w.]+\\.)?SchemaField\\s*\\(\\s*\\{/g;\n for (const m of content.matchAll(startRe)) {\n if (m.index === undefined) continue;\n const fieldName = m[1] ?? '';\n const openIdx = m.index + m[0].length - 1;\n let depth = 0;\n let endIdx = -1;\n for (let i = openIdx; i < content.length; i += 1) {\n if (content[i] === '{') depth += 1;\n else if (content[i] === '}') {\n depth -= 1;\n if (depth === 0) {\n endIdx = i;\n break;\n }\n }\n }\n if (endIdx < 0) continue;\n out.push({\n path: buildSchemaPath(content, m.index, fieldName),\n body: content.slice(openIdx + 1, endIdx),\n index: m.index,\n });\n }\n return out;\n}\n\n/**\n * Extract identifier keys declared at depth 0 of an object-literal body.\n *\n * The naive `value:`/`max:` regex check matches nested constructor options\n * too — `value: new NumberField({ max: 100 })` looked to a flat regex like\n * both keys were SchemaField siblings. Walking the body byte-by-byte with\n * depth tracking + string-boundary handling correctly distinguishes\n * top-level keys from nested ones.\n */\nfunction extractTopLevelKeys(body: string): Set<string> {\n const keys = new Set<string>();\n let depth = 0;\n let inString: '\"' | \"'\" | '`' | null = null;\n let escaped = false;\n let pos = 0;\n const idStart = /[a-zA-Z_$]/;\n const idCont = /[\\w$]/;\n const ws = /\\s/;\n while (pos < body.length) {\n const ch = body[pos];\n if (ch === undefined) break;\n if (escaped) {\n escaped = false;\n pos += 1;\n continue;\n }\n if (inString) {\n if (ch === '\\\\') escaped = true;\n else if (ch === inString) inString = null;\n pos += 1;\n continue;\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch;\n pos += 1;\n continue;\n }\n if (ch === '{' || ch === '(' || ch === '[') {\n depth += 1;\n pos += 1;\n continue;\n }\n if (ch === '}' || ch === ')' || ch === ']') {\n depth -= 1;\n pos += 1;\n continue;\n }\n if (depth === 0 && idStart.test(ch)) {\n let end = pos;\n while (end < body.length && idCont.test(body[end] ?? '')) end += 1;\n const name = body.slice(pos, end);\n let after = end;\n while (after < body.length && ws.test(body[after] ?? '')) after += 1;\n if (body[after] === ':') {\n keys.add(name);\n pos = after + 1;\n continue;\n }\n pos = end;\n continue;\n }\n pos += 1;\n }\n return keys;\n}\n\n/**\n * Load every manifest at the project root, then return:\n * - declared.perSubtype: per `Doc.subtype` htmlFields / filePathFields sets\n * - declared.globalHtml / globalFilePath: union across all subtypes\n * - manifest path/raw/parsed for the manifest we found first\n *\n * Used by rules 004 and 007. The per-subtype map lets rule 004 do a strict\n * cross-check when it knows which subtype a source class is registered to;\n * the global union is the fallback when no class→subtype mapping is found.\n */\nasync function collectDeclaredRichFields(cwd: string): Promise<{\n declared: DeclaredFields;\n manifestPath: string | null;\n manifestRaw: string | null;\n manifestParsed: Record<string, unknown> | null;\n}> {\n const perSubtype = new Map<string, { html: Set<string>; filePath: Set<string> }>();\n const globalHtml = new Set<string>();\n const globalFilePath = new Set<string>();\n let manifestPath: string | null = null;\n let manifestRaw: string | null = null;\n let manifestParsed: Record<string, unknown> | null = null;\n let packageId: string | null = null;\n\n for (const file of ['system.json', 'module.json']) {\n const p = join(cwd, file);\n if (!existsSync(p)) continue;\n let raw: string;\n try {\n raw = await readFile(p, 'utf8');\n } catch {\n continue;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n continue;\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue;\n if (!manifestPath) {\n manifestPath = p;\n manifestRaw = raw;\n manifestParsed = parsed as Record<string, unknown>;\n const id = (parsed as Record<string, unknown>).id;\n packageId = typeof id === 'string' && id.length > 0 ? id : null;\n }\n const docTypes = (parsed as Record<string, unknown>).documentTypes;\n if (!docTypes || typeof docTypes !== 'object' || Array.isArray(docTypes)) continue;\n for (const [docName, docType] of Object.entries(docTypes as Record<string, unknown>)) {\n if (!docType || typeof docType !== 'object' || Array.isArray(docType)) continue;\n for (const [subName, subtype] of Object.entries(docType as Record<string, unknown>)) {\n if (!subtype || typeof subtype !== 'object' || Array.isArray(subtype)) continue;\n const sub = subtype as Record<string, unknown>;\n const key = `${docName}.${subName}`;\n const entry = perSubtype.get(key) ?? {\n html: new Set<string>(),\n filePath: new Set<string>(),\n };\n if (Array.isArray(sub.htmlFields)) {\n for (const e of sub.htmlFields) {\n if (typeof e === 'string') {\n const normalized = normalizeDeclaredPath(e);\n entry.html.add(normalized);\n globalHtml.add(normalized);\n }\n }\n }\n // `filePathFields` is an object, not an array: its KEYS are the\n // field paths and each value lists the file categories allowed\n // there. `htmlFields` above really is a flat array — the two\n // differ, and reading both as arrays drops every correctly\n // declared path.\n for (const declaredPath of declaredFilePathKeys(sub.filePathFields)) {\n const normalized = normalizeDeclaredPath(declaredPath);\n entry.filePath.add(normalized);\n globalFilePath.add(normalized);\n }\n perSubtype.set(key, entry);\n }\n }\n }\n return {\n declared: { perSubtype, packageId, globalHtml, globalFilePath },\n manifestPath,\n manifestRaw,\n manifestParsed,\n };\n}\n\n/**\n * Read the declared file-path keys off one subtype's `filePathFields`.\n *\n * The manifest shape is an object keyed by field path, whose values name\n * the permitted file categories. Older hand-written manifests sometimes\n * carry a bare array instead; that shape does not validate, but reading it\n * costs nothing and avoids reporting a field as undeclared when the author\n * plainly declared it.\n */\nfunction declaredFilePathKeys(value: unknown): string[] {\n if (Array.isArray(value)) {\n return value.filter((e): e is string => typeof e === 'string');\n }\n if (value && typeof value === 'object') {\n return Object.keys(value as Record<string, unknown>);\n }\n return [];\n}\n\n/**\n * Aggregate every `CONFIG.<Doc>.dataModels.<subtype> = <ClassName>`\n * mapping discovered across all source files, keyed by class name.\n * Rule 004 reads this to determine which subtype each rich-field's\n * enclosing class is registered under.\n */\nfunction buildClassToSubtypes(fileContents: Iterable<string>): Map<string, ConfigMapping[]> {\n const map = new Map<string, ConfigMapping[]>();\n for (const content of fileContents) {\n for (const mapping of findConfigMappings(content)) {\n if (!mapping.className) continue;\n const existing = map.get(mapping.className) ?? [];\n existing.push(mapping);\n map.set(mapping.className, existing);\n }\n }\n return map;\n}\n\n/** Strip a dot-path to its final segment (e.g. `system.biography` → `biography`). */\nfunction lastSegment(path: string): string {\n const idx = path.lastIndexOf('.');\n return idx < 0 ? path : path.slice(idx + 1);\n}\n\n/** Entry point: gather everything once, then dispatch to the per-file rules. */\nexport async function runSourceRules(cwd: string): Promise<RuleResult[]> {\n if (!existsSync(cwd)) return [];\n const info = await stat(cwd);\n if (!info.isDirectory()) return [];\n\n const { declared, manifestPath, manifestRaw, manifestParsed } =\n await collectDeclaredRichFields(cwd);\n const sourceFiles: string[] = [];\n for await (const file of walkSourceFiles(cwd)) {\n sourceFiles.push(file);\n }\n\n // Read every source file once so rule 004's class→subtype map can scan\n // across the whole project (CONFIG mappings frequently live in `main.ts`\n // separate from the data-model files).\n const contents = new Map<string, string>();\n for (const file of sourceFiles) {\n try {\n contents.set(file, await readFile(file, 'utf8'));\n } catch {\n // skip unreadable files\n }\n }\n const classToSubtypes = buildClassToSubtypes(contents.values());\n\n const results: RuleResult[] = [];\n for (const [file, content] of contents) {\n results.push(...rule005(file, content));\n results.push(...rule006(file, content));\n results.push(...rule004(file, content, declared, classToSubtypes));\n }\n results.push(\n ...(await rule007(manifestPath, manifestRaw, manifestParsed, sourceFiles, classToSubtypes)),\n );\n return results;\n}\n\n// Internal helpers exported so tests can drive the file walker without\n// pulling in the orchestrator's manifest dependency.\nexport const _internal = {\n walkSourceFiles,\n isSourceFile,\n lastSegment,\n EXCLUDED_DIRS,\n};\n","/**\n * Shared types for the `vttforge audit` rule engine.\n *\n * The audit catalog covers seven v13 footguns from the VTTForge audit\n * spec. Each rule produces zero or more `RuleResult` entries; the\n * orchestrator aggregates them into an `AuditReport` which the reporter\n * renders as JSON or markdown.\n *\n * The CLI never touches the source tree — audit is purely read-only.\n */\n\nexport type Severity = 'HIGH' | 'MEDIUM' | 'LOW';\n\nexport interface RuleResult {\n /** Stable identifier — used by users to suppress / pin / look up specific rules. */\n ruleId: string;\n /** Human title. Shown in markdown report headers. */\n title: string;\n severity: Severity;\n /** Absolute or project-relative path of the file the finding refers to. */\n filePath: string;\n /** Optional line number (1-based) inside `filePath`. */\n line?: number;\n /** One-line description of what was found. */\n message: string;\n /** Optional follow-up describing how to fix it. */\n remediation?: string;\n}\n\nexport interface AuditReport {\n /** Project root the audit ran against. */\n cwd: string;\n /** ISO timestamp of when the run started. */\n startedAt: string;\n /** Findings, ordered by severity then by ruleId. */\n findings: ReadonlyArray<RuleResult>;\n /** Per-severity tallies, for exit-code logic and report headers. */\n counts: { HIGH: number; MEDIUM: number; LOW: number };\n}\n\n/** Severity rank for sorting (HIGH first). */\nexport const SEVERITY_RANK: Record<Severity, number> = {\n HIGH: 0,\n MEDIUM: 1,\n LOW: 2,\n};\n\n/**\n * A rule is a plain function over the project root. Manifest rules parse\n * `system.json` / `module.json`; source rules walk `scripts/` / `src/`.\n * Rules return their own findings — the orchestrator stitches them\n * together. Pure functions over the filesystem so tests inject tmp dirs.\n */\n// (kept here for tests/library consumers that want to define their own rules)\nexport type RuleFn = (cwd: string) => Promise<RuleResult[]> | RuleResult[];\n","/**\n * Audit orchestrator — runs every rule against a project root and\n * aggregates the findings into an `AuditReport`.\n *\n * Rule organisation:\n * - manifest-rules.ts → VTTF-AUDIT-001, 002, 003 (manifest-only)\n * - source-rules.ts → VTTF-AUDIT-004, 005, 006, 007 (source walker\n * + cross-check with manifest where needed)\n *\n * The orchestrator stays minimal — it knows which rule sets exist, but\n * the rules themselves are responsible for their own file IO. This keeps\n * the individual rule files testable in isolation.\n */\n\nimport { existsSync, statSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { runManifestRules } from './manifest-rules.js';\nimport { runSourceRules } from './source-rules.js';\nimport { type AuditReport, type RuleResult, SEVERITY_RANK } from './types.js';\n\nexport interface RunAuditOptions {\n cwd: string;\n}\n\nexport class AuditTargetError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AuditTargetError';\n }\n}\n\nexport async function runAudit(options: RunAuditOptions): Promise<AuditReport> {\n const cwd = resolve(options.cwd);\n // A missing or non-directory target must fail loudly. Otherwise both\n // rule sets return zero findings, the CLI prints \"No issues found\" and\n // exits 0 — silently auditing nothing in CI. The mistyped-path failure\n // mode is invisible without this check.\n if (!existsSync(cwd)) {\n throw new AuditTargetError(`Audit target does not exist: ${cwd}`);\n }\n if (!statSync(cwd).isDirectory()) {\n throw new AuditTargetError(`Audit target is not a directory: ${cwd}`);\n }\n const startedAt = new Date().toISOString();\n\n const [manifestFindings, sourceFindings] = await Promise.all([\n runManifestRules(cwd),\n runSourceRules(cwd),\n ]);\n\n const findings: RuleResult[] = [...manifestFindings, ...sourceFindings].sort(compareFindings);\n\n return {\n cwd,\n startedAt,\n findings,\n counts: countBySeverity(findings),\n };\n}\n\n/** Sort: HIGH first, then by ruleId for deterministic output. */\nfunction compareFindings(a: RuleResult, b: RuleResult): number {\n const sev = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];\n if (sev !== 0) return sev;\n if (a.ruleId !== b.ruleId) return a.ruleId.localeCompare(b.ruleId);\n return a.filePath.localeCompare(b.filePath);\n}\n\nfunction countBySeverity(findings: ReadonlyArray<RuleResult>): AuditReport['counts'] {\n const counts = { HIGH: 0, MEDIUM: 0, LOW: 0 };\n for (const f of findings) counts[f.severity] += 1;\n return counts;\n}\n","/**\n * Render an `AuditReport` as JSON (machine-readable, stable schema) or\n * markdown (terminal-friendly, sectioned by severity).\n *\n * JSON output is the contract for downstream CI tooling: stable shape,\n * stable severity strings, stable ruleId namespace `VTTF-AUDIT-NNN`. The\n * markdown variant exists so a human running `vttforge audit` in a\n * scrollback gets immediate signal without piping through `jq`.\n */\n\nimport { relative } from 'node:path';\nimport type { AuditReport, RuleResult } from './types.js';\n\nexport type ReportFormat = 'json' | 'markdown';\n\nexport function formatReport(report: AuditReport, format: ReportFormat): string {\n return format === 'json' ? formatJson(report) : formatMarkdown(report);\n}\n\nfunction formatJson(report: AuditReport): string {\n return `${JSON.stringify(report, null, 2)}\\n`;\n}\n\nfunction formatMarkdown(report: AuditReport): string {\n const { findings, counts, cwd } = report;\n const lines: string[] = [];\n lines.push('# vttforge audit report');\n lines.push('');\n lines.push(`- cwd: \\`${cwd}\\``);\n lines.push(`- started: ${report.startedAt}`);\n lines.push(\n `- findings: ${findings.length} (${counts.HIGH} HIGH · ${counts.MEDIUM} MEDIUM · ${counts.LOW} LOW)`,\n );\n lines.push('');\n if (findings.length === 0) {\n lines.push('No issues found. The system / module looks healthy against the v13 catalog.');\n lines.push('');\n return lines.join('\\n');\n }\n\n for (const severity of ['HIGH', 'MEDIUM', 'LOW'] as const) {\n const bucket = findings.filter((f) => f.severity === severity);\n if (bucket.length === 0) continue;\n lines.push(`## ${severity} (${bucket.length})`);\n lines.push('');\n for (const finding of bucket) {\n lines.push(formatFinding(finding, cwd));\n lines.push('');\n }\n }\n return lines.join('\\n');\n}\n\nfunction formatFinding(finding: RuleResult, cwd: string): string {\n const location = formatLocation(finding, cwd);\n const lines: string[] = [];\n lines.push(`### \\`${finding.ruleId}\\` — ${finding.title}`);\n lines.push('');\n lines.push(`- file: ${location}`);\n lines.push(`- message: ${finding.message}`);\n if (finding.remediation) {\n lines.push(`- fix: ${finding.remediation}`);\n }\n return lines.join('\\n');\n}\n\nfunction formatLocation(finding: RuleResult, cwd: string): string {\n const rel = relativizeIfBelow(finding.filePath, cwd);\n return finding.line !== undefined ? `\\`${rel}:${finding.line}\\`` : `\\`${rel}\\``;\n}\n\n/**\n * Show paths relative to `cwd` when they live underneath it (cleaner reports),\n * fall back to the absolute path otherwise so we don't lie about where the\n * file actually is.\n */\nfunction relativizeIfBelow(filePath: string, cwd: string): string {\n const rel = relative(cwd, filePath);\n if (rel === '' || rel.startsWith('..') || rel.startsWith('/')) {\n return filePath;\n }\n return rel;\n}\n","/**\n * `vttforge audit` — scan a system/module project against the VTTForge\n * audit catalog (seven v13 manifest + code footguns).\n *\n * The seven rules (VTTF-AUDIT-001 through 007) live in\n * `audit/manifest-rules.ts` or `audit/source-rules.ts`; this file is the\n * CLI surface that orchestrates them and prints the report.\n *\n * Exit codes:\n * 0 — clean run, or only MEDIUM/LOW findings (informational)\n * 1 — at least one HIGH finding (or any finding in `--strict` mode)\n */\n\nimport { resolve } from 'node:path';\nimport { runAudit } from '../audit/index.js';\nimport { formatReport, type ReportFormat } from '../audit/reporter.js';\nimport type { AuditReport } from '../audit/types.js';\n\nexport interface AuditOptions {\n /** Project root to scan. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Output format. Default: 'markdown'. */\n format?: ReportFormat;\n /**\n * When true, MEDIUM and LOW findings also trigger a non-zero exit.\n * Default false — HIGH-only is the canonical \"block CI\" line.\n */\n strict?: boolean;\n /** Custom writer (tests). Defaults to process.stdout.write. */\n write?: (chunk: string) => void;\n}\n\nexport interface AuditResult {\n report: AuditReport;\n /** Suggested exit code under the current strictness setting. */\n exitCode: 0 | 1;\n}\n\nexport async function runAuditCommand(options: AuditOptions = {}): Promise<AuditResult> {\n const cwd = resolve(options.cwd ?? process.cwd());\n const format: ReportFormat = options.format ?? 'markdown';\n const strict = options.strict === true;\n const write =\n options.write ??\n ((chunk: string) => {\n process.stdout.write(chunk);\n });\n\n const report = await runAudit({ cwd });\n write(formatReport(report, format));\n\n const exitCode: 0 | 1 =\n report.counts.HIGH > 0 || (strict && (report.counts.MEDIUM > 0 || report.counts.LOW > 0))\n ? 1\n : 0;\n\n return { report, exitCode };\n}\n","/**\n * Read a built manifest (`dist/system.json` or `dist/module.json`).\n *\n * The vite plugin emits the manifest into `dist/` during build, so the CLI\n * commands that come after a build (dev's initial symlink, build's release\n * zip) read from there rather than walking source. This file is intentionally\n * tiny — we extract just the fields the CLI cares about and surface the\n * rest as `raw` for any consumer that wants more.\n */\n\nimport { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport type PackageType = 'system' | 'module';\n\nexport interface FoundryManifest {\n /** Manifest id — folder name Foundry serves the package under. */\n id: string;\n /** Semver-ish version string from the manifest. */\n version: string;\n /** Detected from which filename was present in dist/. */\n type: PackageType;\n /** Full parsed JSON for callers that need extra fields (compatibility, etc.). */\n raw: Record<string, unknown>;\n}\n\nconst MANIFEST_FILES: ReadonlyArray<{ file: string; type: PackageType }> = [\n { file: 'system.json', type: 'system' },\n { file: 'module.json', type: 'module' },\n];\n\n// Foundry package ids are folder names. We mirror what `vttforge init`\n// allows (lowercase letters, digits, dashes, plus dot/underscore for\n// legacy packages) and reject anything that would let `join()` escape\n// the intended Data/<systems|modules>/ folder — slashes, backslashes,\n// `..` traversal, null bytes, whitespace, control chars.\nconst MANIFEST_ID_RE = /^[a-z][a-z0-9._-]*$/;\n// Version strings end up in the release zip filename. Allow the common\n// semver alphabet (`1.2.3-beta.1+abc123`) and reject anything else for\n// the same reason — `join(cwd, `${id}-${version}.zip`)` must not climb\n// out of cwd.\nconst MANIFEST_VERSION_RE = /^[0-9A-Za-z._+-]+$/;\n\n/**\n * Locate and parse the Foundry manifest inside the given dist directory.\n * Prefers `system.json`, falls back to `module.json` (a single dist can't\n * be both — the vite plugin emits exactly one based on the project type).\n *\n * Throws if neither file exists, required fields are missing/non-string,\n * or the id/version contain characters that would let downstream `join()`\n * calls escape their intended directory.\n */\nexport async function readManifest(distDir: string): Promise<FoundryManifest> {\n for (const { file, type } of MANIFEST_FILES) {\n const path = join(distDir, file);\n if (!existsSync(path)) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(path, 'utf8'));\n } catch (err) {\n throw new Error(\n `Failed to parse ${path}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(`Manifest at ${path} is not a JSON object.`);\n }\n const raw = parsed as Record<string, unknown>;\n const id = raw.id;\n const version = raw.version;\n if (typeof id !== 'string' || id.length === 0) {\n throw new Error(`Manifest at ${path} is missing a non-empty \"id\" field.`);\n }\n if (!MANIFEST_ID_RE.test(id)) {\n throw new Error(\n `Manifest at ${path} has an invalid \"id\" (${JSON.stringify(id)}). Foundry package ids must match ${MANIFEST_ID_RE} — lowercase letters, digits, dashes, dots, underscores; must start with a letter.`,\n );\n }\n if (typeof version !== 'string' || version.length === 0) {\n throw new Error(`Manifest at ${path} is missing a non-empty \"version\" field.`);\n }\n if (!MANIFEST_VERSION_RE.test(version)) {\n throw new Error(\n `Manifest at ${path} has an invalid \"version\" (${JSON.stringify(version)}). Use a semver-ish string with letters, digits, dots, dashes, plus, or underscore.`,\n );\n }\n return { id, version, type, raw };\n }\n throw new Error(\n `No Foundry manifest found in ${distDir}. Expected system.json or module.json — did you run \\`vite build\\` first?`,\n );\n}\n","/**\n * Detect which package manager invoked the CLI so we can run the right\n * `install` command after scaffolding.\n *\n * Strategy: read `npm_config_user_agent`, which pnpm/npm/bun/yarn all set when\n * they spawn a child process (including `pnpm dlx`, `pnpm create`, etc.).\n * Falls back to `pnpm` because that's the VTTForge house default and the\n * most common Foundry-developer choice — but any concrete signal in the\n * environment wins over the default.\n */\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport type PackageManager = 'pnpm' | 'npm' | 'bun' | 'yarn';\n\nexport function detectPackageManager(env: NodeJS.ProcessEnv = process.env): PackageManager {\n const ua = env.npm_config_user_agent;\n if (typeof ua === 'string' && ua.length > 0) {\n if (ua.startsWith('pnpm')) return 'pnpm';\n if (ua.startsWith('bun')) return 'bun';\n if (ua.startsWith('yarn')) return 'yarn';\n if (ua.startsWith('npm')) return 'npm';\n }\n return 'pnpm';\n}\n\nexport function installCommand(pm: PackageManager): string {\n return `${pm} install`;\n}\n\n/**\n * Detect a project's package manager by looking at which lockfile it\n * ships. `dev` and `build` need this — by the time the user runs\n * `vttforge dev`, npm_config_user_agent reflects whatever shell launched\n * the binary (often nothing in a global install), not how the project\n * was bootstrapped. The lockfile is the project's own contract.\n *\n * Falls back to {@link detectPackageManager} for fresh checkouts that\n * haven't been installed yet.\n */\nexport function detectProjectPackageManager(\n cwd: string,\n env: NodeJS.ProcessEnv = process.env,\n): PackageManager {\n if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';\n // Yarn 1 ships .lock; Yarn 2+ Plug'n'Play also keeps yarn.lock at root.\n if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn';\n // Bun text format (bun.lock) supersedes the older binary bun.lockb in 1.1+.\n if (existsSync(join(cwd, 'bun.lock')) || existsSync(join(cwd, 'bun.lockb'))) return 'bun';\n // npm supports both package-lock.json (common) and npm-shrinkwrap.json\n // (published-package convention) — accept either as evidence of an npm\n // project, otherwise we'd misclassify shrinkwrap projects as pnpm.\n if (existsSync(join(cwd, 'package-lock.json')) || existsSync(join(cwd, 'npm-shrinkwrap.json'))) {\n return 'npm';\n }\n return detectPackageManager(env);\n}\n\n/**\n * Argument vector to invoke `vite` (or any other locally-installed CLI)\n * via the project's package manager. We always shell out through `<pm>\n * exec` so the call works regardless of the linker's choices — Yarn 4\n * Plug'n'Play, pnpm's symlinked layout, npm's flattened node_modules,\n * Bun's symlinked node_modules. Each manager exposes a uniform \"run a\n * locally-resolved binary in the project's dependency graph\" command.\n */\nexport function execInvocation(pm: PackageManager, bin: string): [string, string[]] {\n switch (pm) {\n case 'pnpm':\n return ['pnpm', ['exec', bin]];\n case 'yarn':\n // `yarn exec` runs in the PnP context; the `--` separator keeps any\n // bin-side flags from being parsed by yarn itself.\n return ['yarn', ['exec', '--', bin]];\n case 'bun':\n // `bun x <bin>` is the equivalent of `npx` — resolves through the\n // project's node_modules and works without a global install.\n return ['bun', ['x', bin]];\n case 'npm':\n return ['npx', ['--no-install', bin]];\n }\n}\n","/**\n * Spawn the user's project-local `vite` through their package manager.\n *\n * We deliberately go through `<pm> exec vite` instead of probing a\n * `node_modules/vite/bin/vite.js` path: Yarn 4 Plug'n'Play stores\n * dependencies in `.yarn/cache/` rather than `node_modules/`, and even\n * within npm/pnpm projects PNP-style linkers may not materialize the\n * binary on disk. The package-manager command always resolves the\n * locally-installed vite correctly regardless of linker.\n */\n\nimport { type ChildProcess, spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { detectProjectPackageManager, execInvocation } from './package-manager.js';\n\nexport class ViteNotInstalledError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ViteNotInstalledError';\n }\n}\n\n/** Spawn args for invoking the user's vite (e.g. `[\"pnpm\", [\"exec\", \"vite\"]]`). */\nexport function resolveViteInvocation(cwd: string): [string, string[]] {\n // Lightweight sanity check: the project must have a `package.json` so we\n // know we're inside a real project root. We do NOT require\n // `node_modules/vite/bin/vite.js` because Yarn PnP intentionally skips\n // creating that file.\n if (!existsSync(join(cwd, 'package.json'))) {\n throw new ViteNotInstalledError(\n `No package.json found at ${cwd}. Run \\`vttforge dev\\` / \\`vttforge build\\` from inside a scaffolded project.`,\n );\n }\n const pm = detectProjectPackageManager(cwd);\n return execInvocation(pm, 'vite');\n}\n\n/** Run `vite build` once and wait for it to exit. Throws on non-zero exit. */\nexport async function runViteBuildOnce(cwd: string): Promise<void> {\n const [bin, baseArgs] = resolveViteInvocation(cwd);\n await new Promise<void>((resolveBuild, rejectBuild) => {\n const child = spawn(bin, [...baseArgs, 'build'], {\n cwd,\n stdio: 'inherit',\n env: process.env,\n // `shell: false` keeps the args list quoted; npx/pnpm/yarn/bun all\n // accept bare argv without shell expansion.\n shell: false,\n });\n child.on('error', (err) => {\n // Surface a friendlier message if the PM binary itself isn't on PATH.\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n rejectBuild(\n new Error(\n `Could not invoke \"${bin}\". Is the project's package manager installed and on PATH?`,\n ),\n );\n } else {\n rejectBuild(err);\n }\n });\n child.on('exit', (code) => {\n if (code === 0) {\n resolveBuild();\n } else {\n rejectBuild(new Error(`vite build exited with code ${code ?? 'unknown'}`));\n }\n });\n });\n}\n\n/**\n * Spawn `vite build --watch` and return the child process. The caller is\n * responsible for waiting on it and tearing it down (the dev command kills\n * it on SIGINT).\n */\nexport function spawnViteWatch(cwd: string): ChildProcess {\n const [bin, baseArgs] = resolveViteInvocation(cwd);\n return spawn(bin, [...baseArgs, 'build', '--watch'], {\n cwd,\n stdio: 'inherit',\n env: process.env,\n shell: false,\n });\n}\n","/**\n * Emit a foundryvtt.com-compatible release zip.\n *\n * foundryvtt.com expects the package contents at the zip root (no wrapper\n * folder), so `unzip release.zip` produces `system.json`, `scripts/`, etc.\n * directly. The vite build output already matches that layout, so we feed\n * `dist/` straight into archiver with `false` as the second arg to disable\n * the wrap-with-source-dir-name default.\n *\n * Optional extras (LICENSE / README / CHANGELOG) ride along when present at\n * the project root and not already inside `dist/`. We never duplicate.\n */\n\nimport { createWriteStream, existsSync } from 'node:fs';\nimport { stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { ZipArchive } from 'archiver';\n\nexport interface EmitZipOptions {\n /** Directory whose contents go into the zip at root. */\n sourceDir: string;\n /** Output zip path. Parent directory must exist. */\n outFile: string;\n /** Filenames (relative to extrasFrom) to add at the zip root if present. */\n extras?: string[];\n /** Where to look for `extras`. Required when `extras` is non-empty. */\n extrasFrom?: string;\n}\n\nexport interface EmitZipResult {\n outFile: string;\n byteSize: number;\n}\n\nexport async function emitZip(opts: EmitZipOptions): Promise<EmitZipResult> {\n const { sourceDir, outFile, extras = [], extrasFrom } = opts;\n if (!existsSync(sourceDir)) {\n throw new Error(`emitZip: sourceDir does not exist: ${sourceDir}`);\n }\n if (extras.length > 0 && !extrasFrom) {\n throw new Error('emitZip: extrasFrom is required when extras are provided.');\n }\n\n await new Promise<void>((resolveZip, rejectZip) => {\n const output = createWriteStream(outFile);\n const archive = new ZipArchive({ zlib: { level: 9 } });\n\n output.on('close', () => resolveZip());\n output.on('error', rejectZip);\n archive.on('error', rejectZip);\n // archiver emits 'warning' for non-fatal recoverable issues (e.g. an\n // extras file disappeared between existsSync and stream open). ENOENT\n // is the only one safe to swallow; everything else is a real failure.\n archive.on('warning', (err) => {\n if (err.code !== 'ENOENT') rejectZip(err);\n });\n\n archive.pipe(output);\n archive.directory(sourceDir, false);\n\n if (extrasFrom) {\n for (const name of extras) {\n const srcPath = join(extrasFrom, name);\n if (!existsSync(srcPath)) continue;\n const distPath = join(sourceDir, name);\n if (existsSync(distPath)) continue;\n archive.file(srcPath, { name });\n }\n }\n\n archive.finalize().catch(rejectZip);\n });\n\n const info = await stat(outFile);\n return { outFile, byteSize: info.size };\n}\n","/**\n * `vttforge build` — emit a foundryvtt.com-ready release zip.\n *\n * Flow:\n * 1. Clean dist/\n * 2. Run `vite build` (production mode)\n * 3. Read the resulting manifest → id + version\n * 4. Zip dist/ + LICENSE/README/CHANGELOG (if present) into\n * `<cwd>/<id>-<version>.zip` with contents at the zip root\n *\n * The release workflows shipped by the templates upload this zip to a\n * GitHub release; the manifest URL points at `releases/latest/download/`\n * so Foundry's auto-update catches new versions.\n */\n\nimport { rm } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport * as p from '@clack/prompts';\nimport { type FoundryManifest, readManifest } from '../manifest.js';\nimport { runViteBuildOnce } from '../vite-runner.js';\nimport { emitZip } from '../zip.js';\n\nexport interface BuildOptions {\n /** Override the project root. Defaults to `process.cwd()`. */\n cwd?: string;\n}\n\n/** Files we copy into the zip at root when they exist at project root. */\nconst RELEASE_ZIP_EXTRAS = ['LICENSE', 'README.md', 'CHANGELOG.md'];\n\n/** Format a byte count as `12 B`, `4.3 KB`, or `1.2 MB`. */\nfunction formatBytes(n: number): string {\n if (!Number.isFinite(n) || n < 0) return `${n} B`;\n const units = ['B', 'KB', 'MB'] as const;\n let v = n;\n let unitIndex = 0;\n while (v >= 1024 && unitIndex < units.length - 1) {\n v /= 1024;\n unitIndex += 1;\n }\n return `${v.toFixed(v >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;\n}\n\n/**\n * Internal building block exposed for tests: assumes dist/ is already\n * populated (vite has run, manifest is present) and just emits the zip.\n */\nexport async function emitReleaseZip(opts: {\n cwd: string;\n}): Promise<{ zipFile: string; byteSize: number; manifest: FoundryManifest }> {\n const cwd = resolve(opts.cwd);\n const distDir = join(cwd, 'dist');\n const manifest = await readManifest(distDir);\n const zipName = `${manifest.id}-${manifest.version}.zip`;\n const zipFile = join(cwd, zipName);\n const { byteSize } = await emitZip({\n sourceDir: distDir,\n outFile: zipFile,\n extras: RELEASE_ZIP_EXTRAS,\n extrasFrom: cwd,\n });\n return { zipFile, byteSize, manifest };\n}\n\nexport async function runBuild(options: BuildOptions = {}): Promise<void> {\n const cwd = options.cwd ? resolve(options.cwd) : process.cwd();\n p.intro('🜲 vttforge build — release zip');\n\n const distDir = join(cwd, 'dist');\n\n // Wipe dist/ so stale files from a previous dev session don't sneak\n // into a tagged release zip.\n await rm(distDir, { recursive: true, force: true });\n\n const buildSpinner = p.spinner();\n buildSpinner.start('vite build (production)');\n try {\n await runViteBuildOnce(cwd);\n buildSpinner.stop('Build complete');\n } catch (err) {\n buildSpinner.stop('Build failed');\n throw err;\n }\n\n const { zipFile, byteSize } = await emitReleaseZip({ cwd });\n\n p.note(`${zipFile}\\n${formatBytes(byteSize)}`, 'Release artifact');\n p.outro(\n `Upload this zip to your release. The release workflow shipped with the template handles tag→GitHub Release → foundryvtt.com manifest update.`,\n );\n}\n","/**\n * Cross-platform symlink helpers for `vttforge dev`.\n *\n * The dev loop drops a symlink from Foundry's `Data/<systems|modules>/<id>/`\n * back to the project's `dist/` so Foundry serves freshly-built files as\n * vite writes them. The win32 path matters: with `type: 'junction'` the\n * operation works on standard Windows shells without Developer Mode, while\n * `type: 'dir'` requires admin. We never silently fall back to copying —\n * Foundry's HMR relies on live file mutation, and a copy snapshot defeats\n * the loop. If the symlink fails, we surface the platform-specific fix.\n */\n\nimport { lstat, mkdir, readlink, symlink, unlink } from 'node:fs/promises';\nimport { dirname, resolve } from 'node:path';\n\nexport interface CreateLinkOptions {\n /**\n * Overwrite an existing symlink that points elsewhere. Real files and\n * directories are never overwritten — that path needs explicit user\n * action outside the tool.\n */\n overwrite?: boolean;\n}\n\n/**\n * Read a symlink target. Returns the absolute path the link points at, or\n * `null` if `path` doesn't exist or isn't a symlink. Relative symlink\n * targets are resolved against the link's own directory (matches the\n * semantics callers expect when comparing to a known absolute source).\n */\nexport async function readLinkTarget(path: string): Promise<string | null> {\n try {\n const info = await lstat(path);\n if (!info.isSymbolicLink()) return null;\n const target = await readlink(path);\n return resolve(dirname(path), target);\n } catch {\n return null;\n }\n}\n\n/**\n * Create a symlink at `target` pointing to `source`. Win32 uses `'junction'`\n * so the call succeeds without elevation; everywhere else uses `'dir'`.\n *\n * Idempotency:\n * - target already points to source → no-op\n * - target points elsewhere → throws unless `options.overwrite`\n * - target is a real file/dir → throws unconditionally\n */\nexport async function createLink(\n target: string,\n source: string,\n options: CreateLinkOptions = {},\n): Promise<void> {\n const absSource = resolve(source);\n const absTarget = resolve(target);\n\n let info: Awaited<ReturnType<typeof lstat>> | null;\n try {\n info = await lstat(absTarget);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n info = null;\n } else {\n throw err;\n }\n }\n\n if (info?.isSymbolicLink()) {\n const existing = await readLinkTarget(absTarget);\n if (existing === absSource) return;\n if (!options.overwrite) {\n throw new Error(\n `Symlink already exists at ${absTarget} pointing to ${existing}. Pass { overwrite: true } or remove it manually.`,\n );\n }\n await unlink(absTarget);\n } else if (info) {\n const kind = info.isDirectory() ? 'directory' : 'file';\n throw new Error(`Refusing to overwrite ${absTarget}: path exists as a ${kind}, not a symlink.`);\n }\n\n await mkdir(dirname(absTarget), { recursive: true });\n\n const linkType: 'dir' | 'junction' = process.platform === 'win32' ? 'junction' : 'dir';\n try {\n await symlink(absSource, absTarget, linkType);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'EPERM' && process.platform === 'win32') {\n throw new Error(\n `Could not create symlink at ${absTarget}: permission denied. On Windows, enable Developer Mode (Settings → Update & Security → For Developers) or relaunch the terminal as administrator.`,\n );\n }\n throw err;\n }\n}\n\n/**\n * Remove a symlink at `path`. No-ops when the path doesn't exist. Throws\n * when the path exists but isn't a symlink — we never unlink real files\n * or directories.\n */\nexport async function removeLink(path: string): Promise<void> {\n let info: Awaited<ReturnType<typeof lstat>>;\n try {\n info = await lstat(path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return;\n throw err;\n }\n if (!info.isSymbolicLink()) {\n const kind = info.isDirectory() ? 'directory' : 'file';\n throw new Error(`Refusing to remove ${path}: it is a ${kind}, not a symlink.`);\n }\n await unlink(path);\n}\n","/**\n * Put `@vttforge/dev-module` where Foundry will find it.\n *\n * Foundry loads a module from a directory under `Data/modules/<id>/` holding\n * `module.json` and the files it names. The published package already has\n * exactly that shape, so the install is a link to the package root — no copy\n * to keep in step, and a `pnpm update` is picked up on the next reload.\n *\n * A container is the other case: it cannot follow a host symlink, so the\n * compose file mounts the same directory instead. `vttforge dev` prints the\n * mount rather than editing anyone's compose file for them.\n */\nimport { existsSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { createLink, readLinkTarget } from './symlink.js';\n\nconst DEV_MODULE_ID = 'vttforge-dev';\n\nexport interface InstallResult {\n /** Where the module now lives, from Foundry's point of view. */\n target: string;\n /** The package directory it points at — what a container should mount. */\n source: string;\n /** False when the link was already correct. */\n changed: boolean;\n}\n\n/**\n * Find the installed package directory.\n *\n * Resolved from the consumer's project so their `node_modules` answers,\n * falling back to this CLI's own resolution — which is what makes it work\n * inside this repository, where the package is a workspace link.\n */\nexport function resolveDevModuleDir(cwd: string): string | null {\n const candidates = [\n join(cwd, 'node_modules', '@vttforge', 'dev-module'),\n // `dist/` of the CLI sits two levels under its package root; the sibling\n // package is one level up from there.\n join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'dev-module'),\n ];\n for (const dir of candidates) {\n if (existsSync(join(dir, 'module.json'))) return dir;\n }\n return null;\n}\n\nexport async function installDevModule(\n modulesDir: string,\n packageDir: string,\n): Promise<InstallResult> {\n const target = join(modulesDir, DEV_MODULE_ID);\n const existing = await readLinkTarget(target);\n if (existing === packageDir) {\n return { target, source: packageDir, changed: false };\n }\n await createLink(target, packageDir, { overwrite: true });\n return { target, source: packageDir, changed: true };\n}\n\n/** The compose line a containerised Foundry needs, ready to paste. */\nexport function composeMountLine(packageDir: string): string {\n return `- ${packageDir}:/data/Data/modules/${DEV_MODULE_ID}:ro`;\n}\n","/**\n * A one-way WebSocket server: `vttforge dev` pushes, the dev module listens.\n *\n * Hand-written rather than pulled from a package. The CLI ships to every\n * consumer, so each dependency is one they install too, and what is needed\n * here is a narrow slice of RFC 6455: accept the upgrade, send unmasked text\n * frames, notice when a client goes away. No client payloads are read, no\n * compression, no extensions.\n */\nimport { createHash } from 'node:crypto';\nimport { createServer, type Server } from 'node:http';\nimport type { Duplex } from 'node:stream';\n\n/** Fixed GUID from RFC 6455 §1.3, concatenated with the client key. */\nconst WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';\n\n/** Frame lengths above these switch to a wider length field. */\nconst LEN_16_BIT = 126;\nconst LEN_64_BIT = 65_536;\n\n/** Opcode 0x8 — the client is closing. */\nconst OPCODE_CLOSE = 0x8;\n\nexport interface DevServer {\n /** Send one message to every connected client. */\n broadcast: (message: string) => void;\n /** Number of clients currently attached — used to say something truthful. */\n clientCount: () => number;\n close: () => Promise<void>;\n /**\n * The port actually bound, which is not always the one requested: port 0\n * asks the OS to choose. Reporting the request back would leave the caller\n * unable to tell anyone where to connect.\n */\n port: number;\n}\n\n/**\n * Compute the handshake response header.\n *\n * Exported because it is the one part of the handshake with a published test\n * vector, and a wrong value fails as a silent non-connection.\n */\nexport function acceptKey(clientKey: string): string {\n return createHash('sha1')\n .update(clientKey + WS_GUID)\n .digest('base64');\n}\n\n/**\n * Encode one text frame, server to client.\n *\n * Server frames are never masked. The payload length picks one of three\n * widths, and getting the boundary wrong corrupts the stream rather than\n * failing loudly — hence the explicit constants and the tests around them.\n */\nexport function encodeTextFrame(message: string): Buffer {\n const payload = Buffer.from(message, 'utf8');\n const len = payload.length;\n\n let header: Buffer;\n if (len < LEN_16_BIT) {\n header = Buffer.from([0x81, len]);\n } else if (len < LEN_64_BIT) {\n header = Buffer.alloc(4);\n header[0] = 0x81;\n header[1] = LEN_16_BIT;\n header.writeUInt16BE(len, 2);\n } else {\n header = Buffer.alloc(10);\n header[0] = 0x81;\n header[1] = 127;\n header.writeBigUInt64BE(BigInt(len), 2);\n }\n return Buffer.concat([header, payload]);\n}\n\n/** Is this inbound frame a close? That is the only opcode worth reading. */\nexport function isCloseFrame(chunk: Buffer): boolean {\n const first = chunk.at(0);\n return first !== undefined && (first & 0x0f) === OPCODE_CLOSE;\n}\n\nexport interface StartOptions {\n port: number;\n /** Loopback by default: this serves a developer's own machine. */\n host?: string;\n}\n\nexport async function startDevServer(options: StartOptions): Promise<DevServer> {\n const sockets = new Set<Duplex>();\n\n // A plain GET gets a flat answer. Anything that is not an upgrade has\n // reached the wrong port, and saying so beats an unexplained hang.\n const server: Server = createServer((_req, res) => {\n res.writeHead(426, { 'Content-Type': 'text/plain' });\n res.end('This port speaks WebSocket only — it is the vttforge dev bridge.\\n');\n });\n\n server.on('upgrade', (req, socket) => {\n const key = req.headers['sec-websocket-key'];\n if (typeof key !== 'string') {\n socket.destroy();\n return;\n }\n socket.write(\n [\n 'HTTP/1.1 101 Switching Protocols',\n 'Upgrade: websocket',\n 'Connection: Upgrade',\n `Sec-WebSocket-Accept: ${acceptKey(key)}`,\n '\\r\\n',\n ].join('\\r\\n'),\n );\n // Nagle would hold small frames back waiting for company; a reload\n // payload should leave immediately. Node types the upgrade socket as a\n // Duplex even though an HTTP upgrade always hands over a TCP socket, so\n // check for the method rather than asserting the wider type away.\n if ('setNoDelay' in socket && typeof socket.setNoDelay === 'function') {\n (socket as Duplex & { setNoDelay: (on: boolean) => void }).setNoDelay(true);\n }\n sockets.add(socket);\n\n const drop = () => {\n sockets.delete(socket);\n socket.destroy();\n };\n socket.on('data', (chunk: Buffer) => {\n if (isCloseFrame(chunk)) drop();\n });\n socket.on('close', () => sockets.delete(socket));\n // A browser tab closing surfaces as an error on some platforms and a\n // close on others. Either way the socket is gone, and neither should\n // reach the top level.\n socket.on('error', drop);\n });\n\n await new Promise<void>((resolveStart, rejectStart) => {\n server.once('error', rejectStart);\n server.listen(options.port, options.host ?? '127.0.0.1', () => {\n server.removeListener('error', rejectStart);\n resolveStart();\n });\n });\n\n const address = server.address();\n const boundPort = typeof address === 'object' && address ? address.port : options.port;\n\n return {\n broadcast: (message) => {\n const frame = encodeTextFrame(message);\n for (const socket of sockets) {\n // One dead socket must not stop the rest from being told.\n try {\n socket.write(frame);\n } catch {\n sockets.delete(socket);\n }\n }\n },\n clientCount: () => sockets.size,\n close: async () => {\n for (const socket of sockets) socket.destroy();\n sockets.clear();\n await new Promise<void>((resolveClose) => server.close(() => resolveClose()));\n },\n port: boundPort,\n };\n}\n","/**\n * Watch the build output and turn each change into a reload payload.\n *\n * Watching `dist/` rather than hooking into Vite is deliberate: Vite rebuilds\n * into that directory whatever its internals look like, so this stays correct\n * across bundler versions and gives the served path directly — `dist/` is what\n * Foundry mounts, so a file's position inside it is its position under\n * `/systems/<id>/`.\n */\nimport { createHash } from 'node:crypto';\nimport { existsSync, type FSWatcher, readdirSync, readFileSync, watch } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { extname, join, posix, sep } from 'node:path';\n\n/** What the dev module knows how to apply without a page reload. */\nconst RELOADABLE = new Set(['css', 'hbs', 'html', 'json']);\n\n/**\n * Editors save in bursts — write, rename, truncate — and a burst should be\n * one reload, not four.\n */\nconst DEBOUNCE_MS = 40;\n\nexport interface WatchOptions {\n distDir: string;\n packageId: string;\n packageType: 'system' | 'module';\n /** Called once per settled change with the JSON frame to send. */\n onPayload: (frame: string) => void;\n onError?: (message: string) => void;\n debounceMs?: number;\n}\n\nexport interface DistWatcher {\n close: () => void;\n}\n\n/**\n * The path Foundry serves this file at.\n *\n * Always POSIX separators — it becomes a URL, and a Windows backslash would\n * not match the `<link>` the browser rendered.\n */\nexport function servedPath(\n relative: string,\n packageId: string,\n packageType: 'system' | 'module',\n): string {\n const normalized = relative.split(sep).join(posix.sep);\n return posix.join(`${packageType}s`, packageId, normalized);\n}\n\n/** Extension without the dot, lowercased. Empty when there is none. */\nexport function reloadableExtension(relative: string): string | null {\n const ext = extname(relative).replace('.', '').toLowerCase();\n return RELOADABLE.has(ext) ? ext : null;\n}\n\nconst digest = (content: string): string => createHash('sha1').update(content).digest('hex');\n\n/**\n * Record what every reloadable file already contains.\n *\n * Vite rewrites its whole output on each build, so without a baseline the\n * first rebuild after startup looks like every file changed at once.\n *\n * Read synchronously and before the watcher exists, on purpose. Doing it\n * asynchronously races the very first change: the walk can pick up a file\n * written a moment ago and then the change event finds a matching hash and\n * drops a real edit. The output tree is small; correctness is worth the wait.\n */\nfunction seedDigests(distDir: string): Map<string, string> {\n const seen = new Map<string, string>();\n const walk = (dir: string, prefix: string): void => {\n let items: Array<{ name: string; isDirectory: () => boolean }>;\n try {\n items = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const item of items) {\n const rel = prefix ? `${prefix}/${item.name}` : item.name;\n if (item.isDirectory()) {\n walk(join(dir, item.name), rel);\n continue;\n }\n if (!reloadableExtension(rel)) continue;\n try {\n seen.set(rel, digest(readFileSync(join(distDir, rel), 'utf8')));\n } catch {\n // Unreadable now; the first change will record it.\n }\n }\n };\n walk(distDir, '');\n return seen;\n}\n\nexport function watchDist(options: WatchOptions): DistWatcher {\n const debounceMs = options.debounceMs ?? DEBOUNCE_MS;\n const pending = new Map<string, ReturnType<typeof setTimeout>>();\n /**\n * Last content seen per file.\n *\n * A rebuild rewrites every output file, changed or not, so the filesystem\n * event alone says \"the bundler ran\", not \"the developer edited this\".\n * Comparing content is what makes a reload mean the second thing — and it\n * matters: a language file rewritten untouched would redraw every open\n * window, undoing the scoped re-render on the file that did change.\n */\n const digests = seedDigests(options.distDir);\n\n const send = async (relative: string): Promise<void> => {\n const extension = reloadableExtension(relative);\n if (!extension) return;\n let content: string;\n try {\n content = await readFile(join(options.distDir, relative), 'utf8');\n } catch {\n // The file was renamed or removed between the event and the read.\n // Nothing to send, and nothing worth interrupting the developer for.\n return;\n }\n\n const hash = digest(content);\n if (digests.get(relative) === hash) return;\n digests.set(relative, hash);\n\n options.onPayload(\n JSON.stringify({\n packageType: options.packageType,\n packageId: options.packageId,\n content,\n path: servedPath(relative, options.packageId, options.packageType),\n extension,\n }),\n );\n };\n\n // Platforms disagree about a missing directory: macOS throws from `watch`,\n // Linux hands back a watcher that reports nothing at all. Checking first\n // gives the same answer everywhere instead of relying on either.\n if (!existsSync(options.distDir)) {\n options.onError?.(`Cannot watch ${options.distDir} — the directory does not exist.`);\n return { close: () => undefined };\n }\n\n let watcher: FSWatcher;\n try {\n watcher = watch(options.distDir, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n const relative = filename.toString();\n clearTimeout(pending.get(relative));\n pending.set(\n relative,\n setTimeout(() => {\n pending.delete(relative);\n void send(relative);\n }, debounceMs),\n );\n });\n } catch (err) {\n options.onError?.(err instanceof Error ? err.message : String(err));\n return { close: () => undefined };\n }\n\n // Platforms disagree about how a bad path surfaces: macOS throws from\n // `watch` itself, Linux hands back a watcher that emits `error` a tick\n // later. Handling only the throw means a missing dist/ fails silently on\n // Linux — which is where CI runs.\n watcher.on('error', (err: unknown) => {\n options.onError?.(err instanceof Error ? err.message : String(err));\n });\n\n return {\n close: () => {\n for (const timer of pending.values()) clearTimeout(timer);\n pending.clear();\n watcher.close();\n },\n };\n}\n","/**\n * Foundry user-data directory discovery + persistence.\n *\n * Foundry stores worlds, systems, and modules under a single user-data\n * directory (`Data/` lives at its root). `vttforge dev` needs to know where\n * that is so it can symlink the built `dist/` into `Data/systems/<id>/` or\n * `Data/modules/<id>/` and let Foundry pick it up.\n *\n * Precedence (top wins):\n * 1. Explicit `--data-dir` flag passed to the command\n * 2. `FOUNDRY_DATA_DIR` env var (useful in Docker/CI)\n * 3. `<project>/.vttforge/config.json :: foundryDataDir`\n * 4. OS default + interactive prompt that saves to (3) for next time\n *\n * The OS-default detection treats `XDG_DATA_HOME` (Linux) and `%LOCALAPPDATA%`\n * (Windows) as part of the OS convention, not as Foundry-specific overrides.\n *\n * Everything here is pure — `platform`, `env`, and `home` flow through\n * options so tests can drive every branch without monkey-patching globals.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { basename, dirname, join, resolve } from 'node:path';\n\n/** Shape persisted to `<project>/.vttforge/config.json`. */\nexport interface VTTForgeConfig {\n foundryDataDir?: string;\n}\n\n/**\n * Return the OS-default Foundry user-data directory, or `null` on unknown\n * platforms / missing platform-specific env vars. Does NOT check existence —\n * callers prompt the user when the suggested path doesn't resolve.\n */\nexport function autoDetectFoundryDataDir(\n platform: NodeJS.Platform = process.platform,\n env: NodeJS.ProcessEnv = process.env,\n home: string = homedir(),\n): string | null {\n switch (platform) {\n case 'darwin':\n return join(home, 'Library', 'Application Support', 'FoundryVTT');\n case 'linux': {\n const xdg = env.XDG_DATA_HOME;\n if (typeof xdg === 'string' && xdg.length > 0) {\n return join(xdg, 'FoundryVTT');\n }\n return join(home, '.local', 'share', 'FoundryVTT');\n }\n case 'win32': {\n const localAppData = env.LOCALAPPDATA;\n if (typeof localAppData === 'string' && localAppData.length > 0) {\n return join(localAppData, 'FoundryVTT');\n }\n return null;\n }\n default:\n return null;\n }\n}\n\n/**\n * Heuristic: a path looks like a Foundry user-data root if it contains a\n * `Data/` subdirectory (canonical Foundry layout) OR if it already contains\n * `systems/` or `modules/` (the path is already the Data folder itself).\n */\nexport function looksLikeFoundryDataDir(path: string): boolean {\n if (!existsSync(path)) return false;\n return (\n existsSync(join(path, 'Data')) ||\n existsSync(join(path, 'systems')) ||\n existsSync(join(path, 'modules'))\n );\n}\n\nconst CONFIG_DIR = '.vttforge';\nconst CONFIG_FILE = 'config.json';\n\n/** Absolute path to the project-local config file (whether or not it exists). */\nexport function configPath(cwd: string): string {\n return join(resolve(cwd), CONFIG_DIR, CONFIG_FILE);\n}\n\n/** Load `<cwd>/.vttforge/config.json` if it exists and is valid JSON. */\nexport async function loadConfig(cwd: string): Promise<VTTForgeConfig | null> {\n const path = configPath(cwd);\n if (!existsSync(path)) return null;\n try {\n const raw = await readFile(path, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as VTTForgeConfig;\n }\n return null;\n } catch {\n // Corrupted JSON — treat as missing rather than crashing the dev loop.\n // The next save (after a successful prompt) overwrites it cleanly.\n return null;\n }\n}\n\n/** Persist `<cwd>/.vttforge/config.json`. Creates the `.vttforge/` dir as needed. */\nexport async function saveConfig(cwd: string, config: VTTForgeConfig): Promise<void> {\n const path = configPath(cwd);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(config, null, 2)}\\n`, 'utf8');\n}\n\n/**\n * Heuristic: is `path` already Foundry's `Data/` folder (vs. its parent)?\n * We treat the path as Data when:\n * - basename is \"Data\" (case-insensitive — macOS/Windows file systems\n * are case-insensitive by default), OR\n * - any of `systems/`, `modules/`, `worlds/` exists inside (proof that\n * Foundry has previously written package folders here).\n *\n * Used by `foundryPackagesDir` so users can supply EITHER the user-data\n * root OR the `Data/` folder directly without us double-nesting `Data/Data`.\n */\nfunction isFoundryDataFolder(path: string): boolean {\n // basename match — case-insensitive so `DATA`, `data`, `Data` all work on\n // macOS/Windows volumes that don't preserve case strictly. On Linux the\n // folder is always \"Data\" exactly; case-insensitive compare is harmless.\n if (basename(path).toLowerCase() === 'data') return true;\n return (\n existsSync(join(path, 'systems')) ||\n existsSync(join(path, 'modules')) ||\n existsSync(join(path, 'worlds'))\n );\n}\n\n/**\n * Resolve the packages directory (`Data/systems/` or `Data/modules/`) under\n * the given Foundry user-data root. Accepts the path Foundry itself runs\n * with (the user-data folder, which contains `Data/`) OR the `Data/` folder\n * directly — both are common in muscle memory.\n *\n * The returned path is not guaranteed to exist yet; callers `mkdir -p` it\n * before creating the symlink.\n */\nexport function foundryPackagesDir(dataRoot: string, type: 'system' | 'module'): string {\n const subFolder = type === 'system' ? 'systems' : 'modules';\n if (isFoundryDataFolder(dataRoot)) {\n return join(dataRoot, subFolder);\n }\n return join(dataRoot, 'Data', subFolder);\n}\n\n/**\n * Expand `~` / `~/` prefixes to the user's home directory. Shells do this\n * automatically; Node's `path.resolve` does NOT, so a user who types the\n * prompt's own example `~/Library/Application Support/FoundryVTT` would\n * end up with a symlink under `<cwd>/~/Library/...` if we skipped this.\n */\nfunction expandTilde(p: string, home: string): string {\n if (p === '~') return home;\n if (p.startsWith('~/') || p.startsWith('~\\\\')) return join(home, p.slice(2));\n return p;\n}\n\nexport interface ResolveDataDirOptions {\n /** Project root — also where `.vttforge/config.json` is read/written. */\n cwd: string;\n /** `--data-dir` flag value. Skips persistence and prompting when present. */\n override?: string;\n /**\n * Interactive prompt for the first run. Receives the OS-default path\n * (may be `null` on unknown platforms) and resolves with the chosen path,\n * or `null` if the user cancelled. When undefined, the function refuses\n * to ask and throws — appropriate for non-TTY contexts (CI, scripts).\n */\n prompt?: (autoDetected: string | null) => Promise<string | null>;\n /** Override `process.platform` (tests). */\n platform?: NodeJS.Platform;\n /** Override `process.env` (tests). */\n env?: NodeJS.ProcessEnv;\n /** Override `os.homedir()` (tests). */\n home?: string;\n}\n\n/**\n * Walk the precedence chain and return the chosen Foundry user-data path.\n * Saves the user's choice to the project config on first interactive run so\n * subsequent invocations skip the prompt.\n */\nexport async function resolveFoundryDataDir(opts: ResolveDataDirOptions): Promise<string> {\n const { cwd, override, prompt } = opts;\n const env = opts.env ?? process.env;\n const platform = opts.platform ?? process.platform;\n const home = opts.home ?? homedir();\n\n if (typeof override === 'string' && override.length > 0) {\n return resolve(expandTilde(override, home));\n }\n\n const envValue = env.FOUNDRY_DATA_DIR;\n if (typeof envValue === 'string' && envValue.length > 0) {\n return resolve(expandTilde(envValue, home));\n }\n\n const config = await loadConfig(cwd);\n if (config?.foundryDataDir && existsSync(config.foundryDataDir)) {\n return config.foundryDataDir;\n }\n\n const detected = autoDetectFoundryDataDir(platform, env, home);\n\n if (prompt) {\n const choice = await prompt(detected);\n if (!choice) {\n throw new Error('No Foundry data directory selected — aborting.');\n }\n const chosen = resolve(expandTilde(choice, home));\n await saveConfig(cwd, { ...config, foundryDataDir: chosen });\n return chosen;\n }\n\n // Non-interactive fallback: use the OS default if it actually exists.\n // Otherwise fail loudly with the env/flag/config knobs spelled out.\n if (detected && existsSync(detected)) {\n return detected;\n }\n throw new Error(\n 'Foundry data directory not configured. Set FOUNDRY_DATA_DIR, pass --data-dir, or run `vttforge dev` interactively to save the path to .vttforge/config.json.',\n );\n}\n","/**\n * `vttforge dev` — symlink dist/ into Foundry Data and run vite watch.\n *\n * Flow:\n * 1. Sanity-check the project layout (package.json present)\n * 2. Run `vite build` once to populate dist/\n * 3. Read the resulting manifest → id + system/module type\n * 4. Resolve the Foundry user-data directory (override / env / config /\n * OS default + interactive prompt that saves the choice)\n * 5. Create a symlink: `<dataRoot>/Data/<type>s/<id>` → `<cwd>/dist`\n * 6. Spawn `vite build --watch` with inherited stdio\n * 7. Block until SIGINT/SIGTERM, then clean up the symlink and kill vite\n *\n * Foundry's built-in `--hotReload` server flag wires the `flags.hotReload`\n * manifest entry (extensions + paths) to chokidar; the symlink we drop is\n * what chokidar watches, so saves through vite re-emit and Foundry reloads.\n * No additional client integration needed in this version.\n */\n\nimport type { ChildProcess } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport * as p from '@clack/prompts';\nimport { composeMountLine, installDevModule, resolveDevModuleDir } from '../dev-module-install.js';\nimport { startDevServer } from '../dev-server.js';\nimport { watchDist } from '../dev-watcher.js';\nimport {\n foundryPackagesDir,\n type ResolveDataDirOptions,\n resolveFoundryDataDir,\n} from '../foundry-data-dir.js';\nimport { readManifest } from '../manifest.js';\nimport { createLink, readLinkTarget, removeLink } from '../symlink.js';\nimport { runViteBuildOnce, spawnViteWatch } from '../vite-runner.js';\n\nexport interface DevOptions {\n /** Override the project root. Defaults to `process.cwd()`. */\n cwd?: string;\n /** `--data-dir` flag value. Skips env/config/prompt lookup when set. */\n dataDir?: string;\n /** Port for the hot reload bridge. */\n hmrPort?: number;\n}\n\n/**\n * Where the bridge listens.\n *\n * `@vttforge/dev-module` dials this same number. If the two drift apart the\n * module retries a port nobody is listening on, forever and quietly — so a\n * test asserts they still agree.\n */\nexport const DEFAULT_HMR_PORT = 31_313;\n\ninterface BridgeOptions {\n cwd: string;\n distDir: string;\n dataRoot: string;\n manifest: Awaited<ReturnType<typeof readManifest>>;\n port: number;\n}\n\ninterface Bridge {\n close: () => Promise<void>;\n}\n\n/**\n * Stand up the hot reload bridge: install the companion module, open the\n * socket, watch the build output.\n *\n * Every failure here returns null rather than throwing. A busy port or a\n * missing companion package should cost the developer hot reload, not the\n * dev loop — the build and the symlink are the load-bearing parts, and they\n * already succeeded by the time this runs.\n */\nasync function startHotReloadBridge(opts: BridgeOptions): Promise<Bridge | null> {\n const packageDir = resolveDevModuleDir(opts.cwd);\n if (!packageDir) {\n p.note(\n 'Could not find @vttforge/dev-module. Install it to reload saves in place:\\n pnpm add -D @vttforge/dev-module',\n 'Hot reload unavailable',\n );\n return null;\n }\n\n let server: Awaited<ReturnType<typeof startDevServer>>;\n try {\n server = await startDevServer({ port: opts.port });\n } catch {\n p.note(\n `Port ${opts.port} is in use — another \\`vttforge dev\\` is probably running.\\nPass --hmr-port to use a different one.`,\n 'Hot reload unavailable',\n );\n return null;\n }\n\n const modulesDir = foundryPackagesDir(opts.dataRoot, 'module');\n try {\n const install = await installDevModule(modulesDir, packageDir);\n p.note(\n [\n `Companion module linked → ${install.target}`,\n 'Enable \"VTTForge Dev\" in the world, once.',\n '',\n 'Foundry in a container cannot follow that link. Mount it instead:',\n ` ${composeMountLine(packageDir)}`,\n ].join('\\n'),\n 'Hot reload',\n );\n } catch (err) {\n await server.close();\n p.note(\n `Could not install the companion module: ${err instanceof Error ? err.message : String(err)}`,\n 'Hot reload unavailable',\n );\n return null;\n }\n\n const watcher = watchDist({\n distDir: opts.distDir,\n packageId: opts.manifest.id,\n packageType: opts.manifest.type,\n onPayload: (frame) => server.broadcast(frame),\n onError: (message) => p.note(message, 'Watch error'),\n });\n\n return {\n close: async () => {\n watcher.close();\n await server.close();\n },\n };\n}\n\n/**\n * Compose a clack prompt that resolves to a Foundry user-data path or\n * `null` if the user cancels. Suitable for passing to\n * `resolveFoundryDataDir`.\n */\nfunction createDataDirPrompt(): NonNullable<ResolveDataDirOptions['prompt']> {\n return async (autoDetected) => {\n p.note(\n autoDetected\n ? `Detected Foundry user-data directory:\\n ${autoDetected}`\n : 'Could not auto-detect a Foundry user-data directory on this OS — please type the path.',\n 'First-run setup',\n );\n if (autoDetected) {\n const useDetected = await p.confirm({\n message: 'Use detected directory?',\n initialValue: true,\n });\n if (p.isCancel(useDetected)) return null;\n if (useDetected === true) return autoDetected;\n }\n const custom = await p.text({\n message:\n 'Foundry user-data directory (the folder that contains Data/, e.g. ~/Library/Application Support/FoundryVTT)',\n placeholder: autoDetected ?? '/path/to/FoundryVTT',\n validate: (value: string | undefined) => {\n if (!value || value.trim().length === 0) return 'A path is required';\n return undefined;\n },\n });\n if (p.isCancel(custom)) return null;\n return String(custom).trim();\n };\n}\n\n/**\n * Install signal handlers that fire once. We use `process.once` so a second\n * Ctrl-C while cleanup is in flight propagates as a hard exit instead of\n * being swallowed (the user gave up on graceful shutdown — let them).\n */\nfunction installSignalHandlers(cleanup: () => Promise<void>): () => void {\n let invoked = false;\n const handler = () => {\n if (invoked) return;\n invoked = true;\n void cleanup();\n };\n process.once('SIGINT', handler);\n process.once('SIGTERM', handler);\n return () => {\n process.removeListener('SIGINT', handler);\n process.removeListener('SIGTERM', handler);\n };\n}\n\nexport async function runDev(options: DevOptions = {}): Promise<void> {\n const cwd = options.cwd ? resolve(options.cwd) : process.cwd();\n\n p.intro('🜲 vttforge dev — symlink + watch');\n\n if (!existsSync(join(cwd, 'package.json'))) {\n p.cancel(`No package.json at ${cwd}. Run \\`vttforge dev\\` from inside a scaffolded project.`);\n throw new Error('Missing package.json');\n }\n\n // 1. Initial build\n const buildSpinner = p.spinner();\n buildSpinner.start('vite build (initial)');\n try {\n await runViteBuildOnce(cwd);\n buildSpinner.stop('Initial build complete');\n } catch (err) {\n buildSpinner.stop('Initial build failed');\n throw err;\n }\n\n const distDir = join(cwd, 'dist');\n\n // 2. Read manifest emitted by vite\n const manifest = await readManifest(distDir);\n\n // 3. Resolve Foundry data dir\n const interactive = Boolean(process.stdout.isTTY);\n const dataRoot = await resolveFoundryDataDir({\n cwd,\n override: options.dataDir,\n prompt: interactive ? createDataDirPrompt() : undefined,\n });\n\n // 4. Create symlink. We pass overwrite: true so the dev loop \"just works\"\n // when the user switches between projects with the same id — real\n // files/dirs are still refused inside createLink.\n const packagesDir = foundryPackagesDir(dataRoot, manifest.type);\n const target = join(packagesDir, manifest.id);\n const existingTarget = await readLinkTarget(target);\n if (existingTarget && existingTarget !== distDir) {\n p.note(\n `Replacing stale symlink:\\n ${target}\\n was: ${existingTarget}\\n now: ${distDir}`,\n 'Symlink',\n );\n }\n await createLink(target, distDir, { overwrite: true });\n\n p.note(\n `Linked dist/ → ${target}\\nFoundry serves the package under /${manifest.type}s/${manifest.id}/.`,\n 'Symlinked',\n );\n // 5. Hot reload bridge. Failing to start it must not cost the developer\n // the whole dev loop — the build and the symlink are the load-bearing\n // parts, and a busy port should degrade to \"no hot reload\", not to\n // \"vttforge dev does not run\".\n const bridge = await startHotReloadBridge({\n cwd,\n distDir,\n dataRoot,\n manifest,\n port: options.hmrPort ?? DEFAULT_HMR_PORT,\n });\n\n p.note(\n `${bridge ? 'Saves apply in place — no page refresh.' : 'Hot reload is off; saves need a page refresh.'}\\nCtrl-C to stop.`,\n 'Watching',\n );\n\n // 6. Spawn watcher\n const watcher = spawnViteWatch(cwd);\n\n // 6. Block until SIGINT/SIGTERM\n await new Promise<void>((resolveDev) => {\n const cleanup = async () => {\n try {\n await cleanupDevSymlink({ target, expectedSource: distDir });\n } catch {\n // best-effort — Foundry rediscovers the next time dev runs.\n }\n await bridge?.close();\n if (!watcher.killed) watcher.kill('SIGINT');\n uninstall();\n resolveDev();\n };\n const uninstall = installSignalHandlers(cleanup);\n // If vite watcher dies on its own, surface it but don't auto-clean —\n // the user can re-run vttforge dev to re-establish.\n watcher.on('exit', (code) => {\n if (code !== 0 && code !== null) {\n // eslint-disable-next-line no-console\n console.error(`\\nvite watcher exited with code ${code}.`);\n }\n });\n });\n\n p.outro('Stopped. Symlink cleaned up.');\n}\n\n/**\n * Internal building block exposed for tests: given an already-built `dist/`\n * (vite has run, manifest is present) and a resolved Foundry data dir,\n * create the symlink and return the target path + manifest. Skips the\n * vite spawn and the signal-loop, so tests can drive the orchestration\n * deterministically.\n */\nexport async function setupDevSymlink(opts: { cwd: string; dataRoot: string }): Promise<{\n target: string;\n manifest: Awaited<ReturnType<typeof readManifest>>;\n childProcess?: ChildProcess;\n}> {\n const cwd = resolve(opts.cwd);\n const distDir = join(cwd, 'dist');\n const manifest = await readManifest(distDir);\n const packagesDir = foundryPackagesDir(opts.dataRoot, manifest.type);\n const target = join(packagesDir, manifest.id);\n await createLink(target, distDir, { overwrite: true });\n return { target, manifest };\n}\n\n/**\n * Tear down a dev symlink iff it still points at the expected source.\n * Refuses to remove a symlink that another `vttforge dev` session (or a\n * manual `ln -s`) has since redirected — that link doesn't belong to us\n * and removing it would silently disconnect their setup.\n */\nexport async function cleanupDevSymlink(opts: {\n target: string;\n expectedSource: string;\n}): Promise<void> {\n const current = await readLinkTarget(opts.target);\n if (current === opts.expectedSource) {\n await removeLink(opts.target);\n }\n}\n","/**\n * Git helpers for the init scaffolder. Optional: every step degrades to a\n * warning, never throws. The scaffold completes even if git is missing or\n * the user has no global git config — the user can still `git init`\n * themselves later.\n */\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * Read `git config user.name` to pre-fill the author prompt. Returns\n * `undefined` if git is missing or the value is unset.\n */\nexport async function readGitAuthorName(): Promise<string | undefined> {\n try {\n const { stdout } = await execFileAsync('git', ['config', '--get', 'user.name']);\n const value = stdout.trim();\n return value.length > 0 ? value : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Initialize a fresh git repository in `cwd`, run an initial commit. Errors\n * are swallowed and converted to a return value so the caller can decide\n * whether to warn or move on.\n */\nexport async function initGitRepo(cwd: string): Promise<{ ok: boolean; reason?: string }> {\n try {\n await execFileAsync('git', ['init', '--initial-branch=main'], { cwd });\n await execFileAsync('git', ['add', '.'], { cwd });\n await execFileAsync('git', ['commit', '-m', 'chore: initial scaffold from @vttforge/cli'], {\n cwd,\n });\n return { ok: true };\n } catch (err) {\n return { ok: false, reason: err instanceof Error ? err.message : String(err) };\n }\n}\n","/**\n * Template copy with `{{var}}` placeholder substitution.\n *\n * Templates live under `packages/cli/templates/<variant>/` and are shipped\n * inside the published tarball (see the `files` array in package.json).\n * Each file is read, substituted, and written to the destination directory\n * preserving the relative path. Directories are created lazily on first\n * write.\n *\n * We intentionally avoid Handlebars or any other template engine — the\n * substitution surface is small (a flat string-to-string map), and bundling\n * a dependency for ten lines of regex would be wasteful in a tool whose\n * value is being fast to invoke.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport interface ScaffoldVars {\n ID: string;\n TITLE: string;\n DESCRIPTION: string;\n AUTHOR: string;\n LICENSE: string;\n FOUNDRY_MIN_VERSION: string;\n FOUNDRY_VERIFIED_VERSION: string;\n LOCALE_PREFIX: string;\n YEAR: string;\n}\n\nexport interface ScaffoldOptions {\n /** Absolute path to the template root (e.g. `<cli-pkg>/templates/system-ts`). */\n templateDir: string;\n /** Absolute path to the destination directory. Created if it doesn't exist. */\n destDir: string;\n /** Variables to substitute into `{{NAME}}` placeholders inside template files. */\n vars: ScaffoldVars;\n}\n\nconst PLACEHOLDER_RE = /\\{\\{(\\w+)\\}\\}/g;\n\n/**\n * Replace `{{NAME}}` placeholders in `content` using the `vars` map.\n * Unknown placeholders are passed through unchanged so partially-templated\n * files (e.g. a snippet that contains `{{handlebarsLikeSyntax}}` as literal\n * content) still scaffold without surprises.\n */\nexport function substitute(content: string, vars: ScaffoldVars): string {\n const lookup = vars as unknown as Record<string, string>;\n return content.replace(PLACEHOLDER_RE, (match, key: string) =>\n Object.hasOwn(lookup, key) ? (lookup[key] ?? match) : match,\n );\n}\n\n/**\n * Walk `dir` recursively and yield every file path relative to `dir`.\n */\nasync function* walkRelative(dir: string, base: string = dir): AsyncGenerator<string> {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n yield* walkRelative(full, base);\n } else if (entry.isFile()) {\n yield relative(base, full);\n }\n }\n}\n\n/**\n * Files that should be substituted as text. Binary assets (.png, .ico, …)\n * bypass substitution and are copied byte-for-byte. The list of binary\n * extensions is intentionally small — extend if we add real binary assets\n * to templates.\n */\nconst BINARY_EXTENSIONS = new Set([\n '.png',\n '.jpg',\n '.jpeg',\n '.gif',\n '.webp',\n '.ico',\n '.woff',\n '.woff2',\n '.ttf',\n '.otf',\n]);\n\nfunction isBinaryPath(path: string): boolean {\n const dotIndex = path.lastIndexOf('.');\n if (dotIndex < 0) return false;\n return BINARY_EXTENSIONS.has(path.slice(dotIndex).toLowerCase());\n}\n\n/**\n * Files whose source name in the template is rewritten at scaffold time so\n * npm packaging doesn't strip or rename them. npm transforms `.gitignore`\n * into `.npmignore` on publish, so templates ship `_gitignore` and the\n * scaffolder writes `.gitignore` into the generated project.\n */\nconst SCAFFOLD_PATH_RENAMES = new Map<string, string>([['_gitignore', '.gitignore']]);\n\nfunction rewriteDestRelPath(relPath: string): string {\n const base = relPath.split('/').pop() ?? relPath;\n const replacement = SCAFFOLD_PATH_RENAMES.get(base);\n if (replacement === undefined) return relPath;\n return relPath.slice(0, relPath.length - base.length) + replacement;\n}\n\nexport async function scaffold({ templateDir, destDir, vars }: ScaffoldOptions): Promise<void> {\n if (!existsSync(templateDir)) {\n throw new Error(`[vttforge] template directory does not exist: ${templateDir}`);\n }\n const info = await stat(templateDir);\n if (!info.isDirectory()) {\n throw new Error(`[vttforge] template path is not a directory: ${templateDir}`);\n }\n\n await mkdir(destDir, { recursive: true });\n\n for await (const relPath of walkRelative(templateDir)) {\n const srcPath = join(templateDir, relPath);\n // Apply substitution to the path itself so we can name files like\n // `{{ID}}.code-workspace` if we ever need that — today no template path\n // uses placeholders, but the support is free. Then apply the rename map\n // so packaging-stripped names (e.g. `_gitignore`) land at their real\n // destination (e.g. `.gitignore`).\n const substituted = substitute(relPath, vars);\n const destPath = join(destDir, rewriteDestRelPath(substituted));\n await mkdir(dirname(destPath), { recursive: true });\n\n if (isBinaryPath(srcPath)) {\n const bytes = await readFile(srcPath);\n await writeFile(destPath, bytes);\n continue;\n }\n\n const content = await readFile(srcPath, 'utf8');\n await writeFile(destPath, substitute(content, vars), 'utf8');\n }\n}\n\n/**\n * Resolve the templates directory relative to this module's file URL. tsdown\n * emits `dist/scaffold.mjs`, so `import.meta.url` points there at runtime;\n * we walk up two segments to land at the package root, then descend into\n * `templates/`.\n */\nexport function templatesRoot(): string {\n const here = fileURLToPath(import.meta.url);\n // dist/scaffold.mjs → ../ = dist/, ../../ = package root\n return resolve(here, '..', '..', 'templates');\n}\n","/**\n * `vttforge init` — interactive scaffolder.\n *\n * Honors CLI flags first, prompts for everything missing, then copies the\n * matching template into the destination directory. Optionally `git init`s\n * and runs the detected package manager's install command at the end.\n */\n\nimport { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport * as p from '@clack/prompts';\nimport { initGitRepo, readGitAuthorName } from '../git.js';\nimport { detectPackageManager, installCommand } from '../package-manager.js';\nimport { type ScaffoldVars, scaffold, templatesRoot } from '../scaffold.js';\n\nexport interface InitOptions {\n name?: string;\n type?: 'system' | 'module';\n lang?: 'ts' | 'js';\n /** Manifest id. Defaults to a slug of the directory name. */\n id?: string;\n /** Human-readable title shown in Foundry's setup screens. */\n title?: string;\n /** One-line description for the manifest. */\n description?: string;\n /** Author name for the manifest. Defaults to the local git author. */\n author?: string;\n /** SPDX license id written to package.json. */\n license?: string;\n /**\n * Take the default for anything not supplied instead of asking.\n *\n * Required to scaffold without a terminal — a prompt with nothing to read\n * from waits forever, which is no way to fail.\n */\n yes?: boolean;\n noInstall?: boolean;\n noGit?: boolean;\n /** Override the working directory (test hook). Defaults to `process.cwd()`. */\n cwd?: string;\n}\n\nexport interface ResolvedInitOptions\n extends Required<\n Omit<InitOptions, 'cwd' | 'id' | 'title' | 'description' | 'author' | 'license' | 'yes'>\n > {\n cwd: string;\n dest: string;\n id: string;\n title: string;\n description: string;\n author: string;\n license: string;\n foundryMinVersion: string;\n foundryVerifiedVersion: string;\n templateVariant: TemplateVariant;\n}\n\nexport type TemplateVariant = 'system-ts' | 'system-js' | 'module-ts' | 'module-js';\n\nconst PACKAGE_ID_RE = /^[a-z][a-z0-9-]*$/;\nconst UNSAFE_METADATA_CHARS_RE = /[\"\\\\\\r\\n\\t]/;\n\n// Clack hands validators `string | undefined` — the prompt calls them before\n// anything is typed. Blank metadata is allowed here; only the `Required`\n// variant below rejects it.\nexport function validateMetadata(value: string | undefined): string | undefined {\n if (!value) return undefined;\n // Reject characters that would break JSON string contexts in the\n // generated manifests/i18n catalogues. Apostrophes are fine — every JS\n // string literal in the templates is double-quoted, every JSON value is\n // double-quoted. Backslashes and double quotes need explicit escaping\n // we don't perform during substitution, so we reject them at the\n // prompt instead of producing broken scaffolds.\n if (UNSAFE_METADATA_CHARS_RE.test(value)) {\n return 'Avoid backslashes, double quotes, and line breaks — they break the generated manifest.';\n }\n // Reject `*/` because metadata is interpolated into JS/CSS block-comment\n // headers in the generated files; a stray comment terminator there\n // closes the header early and leaves trailing source garbage.\n if (value.includes('*/')) {\n return 'Avoid `*/` — it closes block comments in the generated source headers.';\n }\n return undefined;\n}\n\nexport function validateRequiredMetadata(value: string | undefined): string | undefined {\n if (!value || value.trim().length === 0) {\n return 'Required — Foundry rejects packages with a blank title.';\n }\n return validateMetadata(value);\n}\n\nexport function validatePackageId(value: string | undefined): string | undefined {\n // Coalesce before the test. `PACKAGE_ID_RE.test(undefined)` coerces to the\n // string \"undefined\", which matches the pattern and would let a missing id\n // through.\n if (!PACKAGE_ID_RE.test(value ?? '')) {\n return 'Package id must start with a letter and contain only lowercase letters, digits, dashes';\n }\n return undefined;\n}\n\n/**\n * Thrown when the scaffolder cannot continue — bad input, an existing\n * destination, a cancelled prompt, etc. `runInit` propagates these instead\n * of calling `process.exit`, so library consumers (tests, other CLIs) can\n * catch and recover. The `vttforge` bin wraps `runInit` in a top-level\n * handler that prints the message and exits with code 1.\n */\nexport class ScaffoldError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ScaffoldError';\n }\n}\n\nfunction isCancelled(value: unknown): boolean {\n return p.isCancel(value);\n}\n\nfunction bail(message: string): never {\n p.cancel(message);\n throw new ScaffoldError(message);\n}\n\nfunction slugify(input: string): string {\n return input\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .replace(/-{2,}/g, '-');\n}\n\nfunction localePrefix(id: string): string {\n return id.toUpperCase().replace(/-/g, '_');\n}\n\nfunction titleCase(input: string): string {\n return input\n .split(/[-_\\s]+/)\n .filter((part) => part.length > 0)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ');\n}\n\n/**\n * Whether we may ask the user anything at all.\n *\n * Clack reads from stdin. With no terminal attached there is nothing to read,\n * and the prompt simply hangs — so a run that cannot ask has to be told every\n * answer up front, or be given `--yes` and take the defaults.\n */\nfunction canPrompt(yes: boolean | undefined): boolean {\n return yes !== true && Boolean(process.stdin.isTTY);\n}\n\nfunction templateVariantFor(type: 'system' | 'module', lang: 'ts' | 'js'): TemplateVariant {\n return `${type}-${lang}` as TemplateVariant;\n}\n\nexport async function runInit(options: InitOptions = {}): Promise<void> {\n const cwd = options.cwd ?? process.cwd();\n const interactive = canPrompt(options.yes);\n p.intro('🜲 vttforge init — scaffold a Foundry v13+ system or module');\n\n // --- name ------------------------------------------------------------------\n let name = options.name?.trim();\n if (!name && !interactive) {\n bail(\n 'A directory name is required when running without prompts. Pass it as the first argument: `vttforge init my-module --yes`.',\n );\n }\n if (!name) {\n const answer = await p.text({\n message: 'Directory name (also the default manifest id)',\n placeholder: 'my-system',\n validate: (value: string | undefined) => {\n const trimmed = value?.trim() ?? '';\n if (trimmed.length === 0) return 'Name is required';\n if (!PACKAGE_ID_RE.test(trimmed)) {\n return 'Use only lowercase letters, digits, and dashes (no spaces, slashes, dots).';\n }\n return undefined;\n },\n });\n if (isCancelled(answer)) bail('Scaffold cancelled.');\n name = String(answer).trim();\n } else if (!PACKAGE_ID_RE.test(name)) {\n bail(\n `Invalid name \"${name}\": use only lowercase letters, digits, and dashes (no spaces, slashes, dots).`,\n );\n }\n const dest = resolve(cwd, name);\n if (existsSync(dest)) {\n bail(`Directory already exists: ${dest}`);\n }\n\n // --- type ------------------------------------------------------------------\n let type = options.type;\n if (type !== 'system' && type !== 'module' && !interactive) {\n type = 'system';\n }\n if (type !== 'system' && type !== 'module') {\n const answer = await p.select({\n message: 'What are you building?',\n options: [\n { value: 'system', label: 'system — defines the game rules (Actor/Item types, sheets)' },\n {\n value: 'module',\n label: 'module — extends an existing system or adds cross-system features',\n },\n ],\n initialValue: 'system',\n });\n if (isCancelled(answer)) bail('Scaffold cancelled.');\n type = answer as 'system' | 'module';\n }\n\n // --- lang ------------------------------------------------------------------\n let lang = options.lang;\n if (lang !== 'ts' && lang !== 'js' && !interactive) {\n lang = 'ts';\n }\n if (lang !== 'ts' && lang !== 'js') {\n const answer = await p.select({\n message: 'Language',\n options: [\n { value: 'ts', label: 'TypeScript' },\n { value: 'js', label: 'JavaScript (.mjs)' },\n ],\n initialValue: 'ts',\n });\n if (isCancelled(answer)) bail('Scaffold cancelled.');\n lang = answer as 'ts' | 'js';\n }\n\n // --- id / title / description / author ------------------------------------\n /**\n * Resolve one metadata field: an explicit flag wins, otherwise ask, and\n * when asking is impossible take the default.\n */\n const resolveField = async (\n provided: string | undefined,\n fallback: string,\n ask: () => Promise<unknown>,\n validate: (value: string) => string | undefined,\n ): Promise<string> => {\n if (provided !== undefined) {\n const trimmed = provided.trim();\n const failure = validate(trimmed);\n if (failure !== undefined) bail(`${failure} (got ${JSON.stringify(provided)})`);\n return trimmed;\n }\n if (!interactive) return fallback;\n const answer = await ask();\n if (isCancelled(answer)) bail('Scaffold cancelled.');\n const value = String(answer).trim();\n return value.length > 0 ? value : fallback;\n };\n\n const defaultId = slugify(name);\n const id = await resolveField(\n options.id,\n defaultId,\n () =>\n p.text({\n message: 'Package id (used as the folder Foundry serves under /<systems|modules>/<id>/)',\n initialValue: defaultId,\n validate: validatePackageId,\n }),\n validatePackageId,\n );\n\n const title = await resolveField(\n options.title,\n titleCase(id),\n () =>\n p.text({\n message: 'Title (human-readable, shown in Foundry setup screens)',\n initialValue: titleCase(id),\n validate: validateRequiredMetadata,\n }),\n validateRequiredMetadata,\n );\n\n const defaultDescription = `A Foundry v13+ ${type} built with VTTForge`;\n const description = await resolveField(\n options.description,\n defaultDescription,\n () =>\n p.text({\n message: 'One-line description',\n placeholder: defaultDescription,\n initialValue: defaultDescription,\n validate: validateMetadata,\n }),\n validateMetadata,\n );\n\n const rawDetectedAuthor = await readGitAuthorName();\n // The git fallback bypasses Clack validation, so apply the same metadata\n // safety check before letting it through. If the local git config has\n // something we'd reject from a prompt, drop the suggestion and let the\n // user type a clean value (or land on the 'Anonymous' default).\n const detectedAuthor =\n rawDetectedAuthor && validateMetadata(rawDetectedAuthor) === undefined\n ? rawDetectedAuthor\n : undefined;\n const author = await resolveField(\n options.author,\n detectedAuthor ?? 'Anonymous',\n () =>\n p.text({\n message: 'Author name (used in the manifest)',\n placeholder: detectedAuthor ?? 'Your Name',\n initialValue: detectedAuthor ?? '',\n validate: validateMetadata,\n }),\n validateMetadata,\n );\n\n const license = await resolveField(\n options.license,\n 'MIT',\n () =>\n p.select({\n message: 'License',\n options: [\n { value: 'MIT', label: 'MIT (recommended for Foundry community packages)' },\n { value: 'Apache-2.0', label: 'Apache-2.0' },\n { value: 'GPL-3.0-or-later', label: 'GPL-3.0-or-later' },\n { value: 'UNLICENSED', label: 'UNLICENSED (private use only)' },\n ],\n initialValue: 'MIT',\n }),\n validateRequiredMetadata,\n );\n\n const foundryMinVersion = '13';\n const foundryVerifiedVersion = '13.341';\n\n // --- scaffold --------------------------------------------------------------\n const templateVariant = templateVariantFor(type, lang);\n const templateDir = resolve(templatesRoot(), templateVariant);\n if (!existsSync(templateDir)) {\n bail(`Template \"${templateVariant}\" not found at ${templateDir}`);\n }\n\n const vars: ScaffoldVars = {\n ID: id,\n TITLE: title,\n DESCRIPTION: description,\n AUTHOR: author,\n LICENSE: license,\n FOUNDRY_MIN_VERSION: foundryMinVersion,\n FOUNDRY_VERIFIED_VERSION: foundryVerifiedVersion,\n LOCALE_PREFIX: localePrefix(id),\n YEAR: String(new Date().getFullYear()),\n };\n\n const scaffoldSpinner = p.spinner();\n scaffoldSpinner.start(`Scaffolding ${templateVariant} into ${name}/`);\n try {\n await scaffold({ templateDir, destDir: dest, vars });\n scaffoldSpinner.stop(`Scaffold written to ${dest}`);\n } catch (err) {\n scaffoldSpinner.stop('Scaffold failed.');\n throw err;\n }\n\n // --- install ---------------------------------------------------------------\n // Install runs BEFORE git init so the lockfile lands in the initial commit\n // — generated repos that ship with a lockfile install reproducibly and\n // the working tree stays clean after scaffold.\n const pm = detectPackageManager();\n let shouldInstall = !options.noInstall;\n if (shouldInstall && interactive) {\n const confirm = await p.confirm({\n message: `Install dependencies now with ${pm}?`,\n initialValue: true,\n });\n if (isCancelled(confirm))\n bail('Scaffold cancelled at install prompt (your scaffold is intact).');\n shouldInstall = confirm === true;\n }\n\n if (shouldInstall) {\n const installSpinner = p.spinner();\n installSpinner.start(`Running ${installCommand(pm)}`);\n try {\n await runInstall(pm, dest);\n installSpinner.stop(`Dependencies installed (${pm}).`);\n } catch (err) {\n installSpinner.stop(`Install failed: ${err instanceof Error ? err.message : String(err)}`);\n p.note(\n `Run \\`${installCommand(pm)}\\` inside ${name}/ to retry.`,\n 'Continuing without install.',\n );\n }\n }\n\n // --- git -------------------------------------------------------------------\n // Runs AFTER install so the resulting lockfile is captured by the first\n // commit and the working tree stays clean.\n if (!options.noGit) {\n const gitSpinner = p.spinner();\n gitSpinner.start('Initializing git repository');\n const gitResult = await initGitRepo(dest);\n if (gitResult.ok) {\n gitSpinner.stop('Git initialized (branch: main, initial commit landed).');\n } else {\n gitSpinner.stop(`Skipped git init: ${gitResult.reason ?? 'unknown error'}`);\n }\n }\n\n // --- outro -----------------------------------------------------------------\n const nextSteps = [\n `cd ${name}`,\n !shouldInstall ? installCommand(pm) : null,\n `${pm} run build`,\n 'Symlink dist/ into your Foundry Data/<systems|modules>/<id>/ (or use the dev compose mount)',\n ]\n .filter((line): line is string => line !== null)\n .map((line, idx) => ` ${idx + 1}. ${line}`)\n .join('\\n');\n p.note(nextSteps, 'Next steps');\n p.outro('Have fun building.');\n}\n\nfunction runInstall(pm: string, cwd: string): Promise<void> {\n return new Promise((resolveInstall, rejectInstall) => {\n const child = spawn(pm, ['install'], {\n cwd,\n stdio: 'inherit',\n env: process.env,\n });\n child.on('error', rejectInstall);\n child.on('exit', (code) => {\n if (code === 0) resolveInstall();\n else rejectInstall(new Error(`${pm} install exited with code ${code}`));\n });\n });\n}\n","/**\n * @vttforge/cli — public library exports.\n *\n * The CLI is intended to be used as a bin (`vttforge` command). These\n * exports exist for consumers who want to programmatically invoke the\n * scaffolder (e.g. from another CLI, a test harness, or a custom tool).\n */\n\nexport { runAudit } from './audit/index.js';\nexport { runManifestRules } from './audit/manifest-rules.js';\nexport type { ReportFormat } from './audit/reporter.js';\nexport { formatReport } from './audit/reporter.js';\nexport { runSourceRules } from './audit/source-rules.js';\nexport type { AuditReport, RuleFn, RuleResult, Severity } from './audit/types.js';\nexport { SEVERITY_RANK } from './audit/types.js';\nexport type { AuditOptions, AuditResult } from './commands/audit.js';\nexport { runAuditCommand } from './commands/audit.js';\nexport type { BuildOptions } from './commands/build.js';\nexport { emitReleaseZip, runBuild } from './commands/build.js';\nexport type { DevOptions } from './commands/dev.js';\nexport { cleanupDevSymlink, runDev, setupDevSymlink } from './commands/dev.js';\nexport type { InitOptions, ResolvedInitOptions, TemplateVariant } from './commands/init.js';\nexport { runInit, ScaffoldError } from './commands/init.js';\nexport type { ResolveDataDirOptions, VTTForgeConfig } from './foundry-data-dir.js';\nexport {\n autoDetectFoundryDataDir,\n configPath,\n foundryPackagesDir,\n loadConfig,\n looksLikeFoundryDataDir,\n resolveFoundryDataDir,\n saveConfig,\n} from './foundry-data-dir.js';\nexport type { FoundryManifest, PackageType } from './manifest.js';\nexport { readManifest } from './manifest.js';\nexport type { PackageManager } from './package-manager.js';\nexport {\n detectPackageManager,\n detectProjectPackageManager,\n execInvocation,\n installCommand,\n} from './package-manager.js';\nexport type { ScaffoldOptions, ScaffoldVars } from './scaffold.js';\nexport { scaffold, substitute, templatesRoot } from './scaffold.js';\nexport type { CreateLinkOptions } from './symlink.js';\nexport { createLink, readLinkTarget, removeLink } from './symlink.js';\nexport {\n resolveViteInvocation,\n runViteBuildOnce,\n spawnViteWatch,\n ViteNotInstalledError,\n} from './vite-runner.js';\nexport type { EmitZipOptions, EmitZipResult } from './zip.js';\nexport { emitZip } from './zip.js';\n\nexport const VTTFORGE_CLI_VERSION = '0.1.0';\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vttforge/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "VTTForge CLI: scaffolding, dev server, build orchestration.",
|
|
5
|
+
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"
|
|
8
|
-
"homepage": "https://github.com/vttforge/vttforge",
|
|
7
|
+
"homepage": "https://github.com/vttforge/vttforge#readme",
|
|
9
8
|
"repository": {
|
|
10
9
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/vttforge/vttforge.git"
|
|
10
|
+
"url": "git+https://github.com/vttforge/vttforge.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
12
|
},
|
|
13
|
-
"bugs":
|
|
14
|
-
|
|
13
|
+
"bugs": "https://github.com/vttforge/vttforge/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"foundryvtt",
|
|
16
|
+
"cli",
|
|
17
|
+
"scaffold"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"vttforge": "./dist/bin.mjs"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"import": "./dist/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.mjs",
|
|
30
|
+
"module": "./dist/index.mjs",
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"templates",
|
|
35
|
+
"README.md",
|
|
36
|
+
"CHANGELOG.md"
|
|
37
|
+
],
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@clack/prompts": "^1.7.0",
|
|
41
|
+
"archiver": "^8.0.0",
|
|
42
|
+
"citty": "^0.2.2"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
46
|
+
"@types/archiver": "^8.0.0",
|
|
47
|
+
"@types/node": "^26.4.0",
|
|
48
|
+
"@vttforge/dev-module": "0.2.0",
|
|
49
|
+
"publint": "^0.3.24",
|
|
50
|
+
"tsdown": "^0.22.14",
|
|
51
|
+
"typescript": "^7.0.2",
|
|
52
|
+
"unrun": "^0.3.1",
|
|
53
|
+
"vitest": "^4.1.11"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=26.0.0"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public",
|
|
60
|
+
"provenance": false
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsdown",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"test": "vitest run --passWithNoTests",
|
|
66
|
+
"publint": "publint",
|
|
67
|
+
"attw": "attw --pack . --profile esm-only"
|
|
15
68
|
}
|
|
16
|
-
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Resolve version from tag
|
|
18
|
+
id: version
|
|
19
|
+
env:
|
|
20
|
+
REF: ${{ github.ref_name }}
|
|
21
|
+
run: |
|
|
22
|
+
version="${REF#v}"
|
|
23
|
+
# Git allows surprising characters in tag names. Reject anything
|
|
24
|
+
# that isn't a strict semver-ish identifier so downstream node -e
|
|
25
|
+
# / shell interpolations can't be turned into command injection
|
|
26
|
+
# by a malicious tag push.
|
|
27
|
+
if ! [[ "$version" =~ ^[0-9A-Za-z._+-]+$ ]]; then
|
|
28
|
+
echo "Refusing tag with unsafe characters: $version" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
32
|
+
|
|
33
|
+
- uses: pnpm/action-setup@v4
|
|
34
|
+
with:
|
|
35
|
+
version: 10
|
|
36
|
+
|
|
37
|
+
- uses: actions/setup-node@v4
|
|
38
|
+
with:
|
|
39
|
+
node-version: 22
|
|
40
|
+
|
|
41
|
+
- run: pnpm install
|
|
42
|
+
|
|
43
|
+
- name: Sync manifest version
|
|
44
|
+
env:
|
|
45
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
46
|
+
run: |
|
|
47
|
+
node -e "const fs=require('node:fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version=process.env.VERSION;fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');"
|
|
48
|
+
|
|
49
|
+
# We invoke vite directly (not `pnpm build` → `vttforge build`) because
|
|
50
|
+
# the release workflow needs to:
|
|
51
|
+
# 1. Run vite build → emit dist/module.json
|
|
52
|
+
# 2. Patch manifest/download URLs into dist/module.json
|
|
53
|
+
# 3. Zip dist/ → release artifact (so the zipped manifest carries URLs)
|
|
54
|
+
# `vttforge build` zips at step 1, which would freeze the un-patched
|
|
55
|
+
# manifest. Eventually the vite-plugin will support env-driven URL
|
|
56
|
+
# injection and this dance disappears.
|
|
57
|
+
- run: pnpm exec vite build
|
|
58
|
+
|
|
59
|
+
- name: Inject release URLs into manifest
|
|
60
|
+
env:
|
|
61
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
62
|
+
REPO: ${{ github.repository }}
|
|
63
|
+
PKG_ID: '{{ID}}'
|
|
64
|
+
run: |
|
|
65
|
+
node -e "const fs=require('node:fs');const path='dist/module.json';const m=JSON.parse(fs.readFileSync(path,'utf8'));const r=process.env.REPO;const v=process.env.VERSION;const id=process.env.PKG_ID;m.manifest=\`https://github.com/\${r}/releases/latest/download/module.json\`;m.download=\`https://github.com/\${r}/releases/download/v\${v}/\${id}-\${v}.zip\`;fs.writeFileSync(path,JSON.stringify(m,null,2)+'\n');"
|
|
66
|
+
|
|
67
|
+
- name: Build release zip
|
|
68
|
+
env:
|
|
69
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
70
|
+
PKG_ID: '{{ID}}'
|
|
71
|
+
run: |
|
|
72
|
+
cd dist
|
|
73
|
+
zip -r "../${PKG_ID}-${VERSION}.zip" .
|
|
74
|
+
|
|
75
|
+
- name: Create GitHub Release
|
|
76
|
+
env:
|
|
77
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
78
|
+
PKG_ID: '{{ID}}'
|
|
79
|
+
uses: softprops/action-gh-release@v2
|
|
80
|
+
with:
|
|
81
|
+
files: |
|
|
82
|
+
${{ env.PKG_ID }}-${{ env.VERSION }}.zip
|
|
83
|
+
dist/module.json
|
|
84
|
+
generate_release_notes: true
|
|
85
|
+
body: |
|
|
86
|
+
## Foundry installation
|
|
87
|
+
|
|
88
|
+
Install via Foundry's setup wizard with this manifest URL:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
https://github.com/${{ github.repository }}/releases/latest/download/module.json
|
|
92
|
+
```
|