-- From dphfox/tiniest, licenced under BSD
--!strict

local tiniest = require("./tiniest")
local tiniest_plugin = require("./tiniest_plugin")
type Test = tiniest.Test
type TestRunResult = tiniest.TestRunResult
type RunResult = tiniest.RunResult

export type Options = {
	plugins: nil | { tiniest_plugin.Plugin },
	disable_emoji: boolean?,
	disable_unicode: boolean?,
	disable_colour: boolean?,
	disable_output: nil | {
		after_run: boolean?,
	},
}

local tiniest_pretty = {}

function tiniest_pretty.configure(options: Options)
	if options.disable_unicode then
		options.disable_emoji = true
	end

	local plugins = tiniest_plugin.configure(options.plugins)

	local function string_len(text: string)
		if options.disable_unicode then
			return string.len(text)
		else
			return utf8.len(text) or string.len(text)
		end
	end
	local status_icons = {
		pass = if options.disable_emoji then "[PASS]" else "✅",
		fail = if options.disable_emoji then "[FAIL]" else "❌",
	}
	local crumb_trail = if options.disable_unicode then " > " else " ▸ "
	local divider = if options.disable_unicode then "=" else "═"
	local margin_line = if options.disable_unicode then "|" else "│"

	local paint = {}
	do
		local function ansi_mode(...)
			if options.disable_colour then
				return ""
			else
				return `{string.char(27)}[{table.concat({ ... }, ";")}m`
			end
		end
		local DIM = ansi_mode("2")
		local PASS = ansi_mode("1", "32")
		local PASS_DIM = ansi_mode("2", "32")
		local FAIL = ansi_mode("1", "31")
		local FAIL_DIM = ansi_mode("2", "31")
		local TRACE = ansi_mode("1", "34")
		local TRACE_DIM = ansi_mode("2", "34")
		local NUMBER = ansi_mode("1", "36")
		local STRING = ansi_mode("1", "33")
		local KEYWORD = ansi_mode("1", "35")
		local RESET = ansi_mode("0")

		function paint.dim(text: string): string
			return DIM .. text .. RESET
		end

		function paint.pass(text: string): string
			return PASS .. text .. RESET
		end

		function paint.pass_dim(text: string): string
			return PASS_DIM .. text .. RESET
		end

		function paint.fail(text: string): string
			return FAIL .. text .. RESET
		end

		function paint.fail_dim(text: string): string
			return FAIL_DIM .. text .. RESET
		end

		function paint.trace(text: string): string
			return TRACE .. text .. RESET
		end

		function paint.trace_dim(text: string): string
			return TRACE_DIM .. text .. RESET
		end

		function paint.number(text: string): string
			return NUMBER .. text .. RESET
		end

		function paint.string(text: string): string
			return STRING .. text .. RESET
		end

		function paint.keyword(text: string): string
			return KEYWORD .. text .. RESET
		end
	end

	local LINE_LENGTH = 80
	local full_width_divider = paint.dim(string.rep(divider, LINE_LENGTH))
	local function title(text: string): string
		local no_ansi = text:gsub(`{string.char(27)}.-m`, "")
		local divider_count = (LINE_LENGTH - string_len(no_ansi) - 2) / 2
		local divider_lhs = string.rep(divider, math.ceil(divider_count))
		local divider_rhs = string.rep(divider, math.floor(divider_count))
		return paint.dim(divider_lhs) .. " " .. text .. " " .. paint.dim(divider_rhs)
	end
	local function syntax(snippet: string): string
		return snippet
			:gsub("0?[xXbB]?%d*%.?%d*[eE]?%-?%d+", paint.number)
			:gsub('".-"', paint.string)
			:gsub("'.-'", paint.string)
			:gsub("function", paint.keyword)
			:gsub("end", paint.keyword)
			:gsub("true", paint.keyword)
			:gsub("false", paint.keyword)
			:gsub("nil", paint.keyword)
			:gsub("%-%-.-\n", paint.dim)
	end
	local function indent(passage: string, indentation: string): string
		return passage:gsub("\n", "\n" .. indentation)
	end

	local self = {}
	self.is_tiniest_plugin = true

	function self.format_run(run_result: RunResult): string
		local lines = {}

		local pretty_results = {}
		for _, test in run_result.tests do
			local result = run_result.individual[test]
			local painted_labels = table.clone(test.labels)
			for index, label in painted_labels do
				local style = result.status .. if index < #painted_labels then "_dim" else ""
				painted_labels[index] = paint[style](label)
			end
			local annotations = ""
			local function add_annotation(annotation: string): ()
				annotations ..= ` - {annotation}`
			end
			plugins.notify("add_annotations", result, options, add_annotation)
			local pretty = {
				test = test,
				result = result,
				crumbs = table.concat(painted_labels, paint[result.status .. "_dim"](crumb_trail)),
				icon = status_icons[result.status],
				annotations = paint.dim(annotations),
			}
			table.insert(pretty_results, pretty)
		end

		if run_result.status_tally.fail > 0 then
			table.insert(lines, title(`Errors from {run_result.status_tally.fail} test(s)`))
			table.insert(lines, "")
			for _, pretty in pretty_results do
				if pretty.result.status == "pass" then
					continue
				end
				table.insert(lines, `{pretty.icon} {pretty.crumbs}`)
				table.insert(lines, paint.trace(pretty.result.error.message))
				local code = pretty.result.error.code
				if code ~= nil then
					local num_length = string_len(code.line)
					local empty_margin = string.rep(" ", num_length + 1)
						.. paint.dim(margin_line)
						.. " "
					table.insert(lines, empty_margin)
					table.insert(
						lines,
						`{paint.dim(`{code.line} {margin_line}`)} {indent(
							syntax(code.snippet),
							empty_margin
						)}`
					)
					table.insert(lines, empty_margin)
				end
				local trace = pretty.result.error.trace:gsub("\n+$", "")
				table.insert(lines, paint.trace_dim(trace))
				table.insert(lines, "")
			end
		end

		local line_items = {}
		local function add_line_items(key: string, value: string): ()
			table.insert(line_items, `{paint.dim(key .. ":")} {value}`)
		end
		plugins.notify("add_line_items", run_result, options, add_line_items)

		table.insert(lines, title(`Status of {#pretty_results} test(s)`))
		table.insert(lines, "")
		for _, pretty in pretty_results do
			table.insert(lines, `{pretty.icon} {pretty.crumbs}{pretty.annotations}`)
		end
		table.insert(lines, "")
		table.insert(
			lines,
			title(
				`{paint.pass(`{run_result.status_tally.pass} passed`)}, {paint.fail(
					`{run_result.status_tally.fail} failed`
				)}`
			)
		)
		if #line_items > 0 then
			table.insert(lines, "")
			for _, line_item in line_items do
				table.insert(lines, line_item)
			end
			table.insert(lines, "")
			table.insert(lines, full_width_divider)
		end

		return table.concat(lines, "\n")
	end

	function self.after_run(run_result: RunResult, _): ()
		if options.disable_output and options.disable_output.after_run then
			return
		end
		print(self.format_run(run_result))
	end

	return self
end

return tiniest_pretty


-- From dphfox/tiniest, licensed under BSD3
--!strict

local tiniest = require("./tiniest")
local tiniest_quote = require("./tiniest_quote")
type Test = tiniest.Test

export type TestRunResult = tiniest.TestRunResult & {
	num_snapshots_updated: number,
	num_snapshots_obsolete: number,
}
export type RunResult = tiniest.RunResult & {
	num_snapshots_updated: number,
	num_snapshots_obsolete: number,
}

export type Options = {
	save_snapshots: nil | (key: string, values: { string }) -> (),
	load_snapshots: nil | (key: string) -> { string }?,
}

type TestContext = {
	key: string,
	snapshots: { string },
	next_snapshot_index: number,
	num_updated: number,
}

type RunContext = {
	num_updated: number,
	num_obsolete: number,
}

local tiniest_snapshot = {}

function tiniest_snapshot.configure(options: Options)
	local self = {}
	self.is_tiniest_plugin = true

	local function get_test_key(test: Test)
		local labels = {}
		for _, label in test.labels do
			label = label:lower():gsub("[^%w%s]", ""):gsub(" ", "_")
			table.insert(labels, label)
		end
		return table.concat(labels, ".")
	end

	local run_context: RunContext?
	local test_context: TestContext?

	function self.snapshot(x: unknown): ()
		if options.load_snapshots == nil then
			error("snapshot() is unavailable - snapshots have not been configured", 0)
		elseif run_context == nil or test_context == nil then
			error("snapshot() can only be used while a test is running with tiniest_snapshot", 0)
		end

		local snapshot_index = test_context.next_snapshot_index
		test_context.next_snapshot_index += 1

		local fresh_snapshot = tiniest_quote(x)
		local snapshot_on_disk = test_context.snapshots[snapshot_index]
		test_context.snapshots[snapshot_index] = fresh_snapshot

		if fresh_snapshot == snapshot_on_disk then
			return
		end

		if options.save_snapshots then
			run_context.num_updated += 1
			test_context.num_updated += 1
			test_context.snapshots[snapshot_index] = fresh_snapshot
		elseif snapshot_on_disk == nil then
			error({
				type = "tiniest.ErrorReport",
				message = "New snapshot() call needs to be saved.\nRun while saving snapshots to save it to disk.",
				trace = debug.traceback(nil, 2),
				code = {
					snippet = `snapshot({fresh_snapshot})`,
					line = debug.info(2, "l"),
				},
			}, 0)
		else
			error({
				type = "tiniest.ErrorReport",
				message = "Snapshot does not match",
				trace = debug.traceback(nil, 2),
				code = {
					snippet = `snapshot({fresh_snapshot})\n\n-- snapshot on disk:\nsnapshot({snapshot_on_disk})`,
					line = debug.info(2, "l"),
				},
			}, 0)
		end
	end

	function self.before_run(_, _): ()
		run_context = {
			num_updated = 0,
			num_obsolete = 0,
		}
	end

	function self.after_run(original_run_result: tiniest.RunResult, _): ()
		assert(run_context ~= nil)
		local run_result = original_run_result :: RunResult
		run_result.num_snapshots_updated = run_context.num_updated
		run_result.num_snapshots_obsolete = run_context.num_obsolete
		run_context = nil
	end

	function self.before_test(test: Test, _): ()
		assert(run_context ~= nil)
		local key = get_test_key(test)
		test_context = {
			key = key,
			snapshots = options.load_snapshots and options.load_snapshots(key) or {},
			next_snapshot_index = 1,
			num_updated = 0,
		}
	end

	function self.after_test(_, original_run_result: tiniest.TestRunResult, _): ()
		assert(run_context ~= nil)
		assert(test_context ~= nil)
		local run_result = original_run_result :: TestRunResult
		run_result.num_snapshots_updated = test_context.num_updated
		run_result.num_snapshots_obsolete = test_context.next_snapshot_index
			- #test_context.snapshots
			- 1
		run_context.num_obsolete += run_result.num_snapshots_obsolete
		if test_context.num_updated > 0 and options.save_snapshots ~= nil then
			options.save_snapshots(test_context.key, test_context.snapshots)
		end
		test_context = nil
	end

	function self.add_annotations(
		original_run_result: tiniest.TestRunResult,
		_,
		add_annotation: (string) -> ()
	)
		local run_result = original_run_result :: TestRunResult
		if run_result.num_snapshots_updated > 0 then
			add_annotation(`{run_result.num_snapshots_updated} snapshot(s) updated`)
		end
		if run_result.num_snapshots_obsolete > 0 then
			add_annotation(`{run_result.num_snapshots_obsolete} snapshot(s) obsolete`)
		end
	end

	function self.add_line_items(
		original_run_result: tiniest.RunResult,
		_,
		add_line_item: (string, string) -> ()
	)
		local run_result = original_run_result :: RunResult
		if run_result.num_snapshots_updated > 0 then
			add_line_item("Updated snapshots", tostring(run_result.num_snapshots_updated))
		end
		if run_result.num_snapshots_obsolete > 0 then
			add_line_item("Obsolete snapshots", tostring(run_result.num_snapshots_obsolete))
		end
	end

	return self
end

return tiniest_snapshot


--!strict

--[[
    Type definitions for the tiniest testing framework Based on dphfox/tiniest,
    licensed under BSD

    This file provides comprehensive type definitions for all tiniest framework
    functions to improve type safety in test files.
]]

-- Core tiniest framework types
export type Test = {
	labels: { string },
	run: () -> (),
}

export type ErrorReport = {
	type: "tiniest.ErrorReport",
	message: string,
	trace: string,
	code: {
		snippet: string,
		line: string,
	}?,
}

export type TestRunResult = PassTestRunResult | FailTestRunResult

export type PassTestRunResult = {
	status: "pass",
}

export type FailTestRunResult = {
	status: "fail",
	error: ErrorReport,
}

export type RunResult = {
	tests: { Test },
	status_tally: {
		pass: number,
		fail: number,
	},
	individual: { [Test]: TestRunResult },
}

-- Expectation chain interface - supports fluent API
export type ExpectChain = {
	-- Existence checks
	exists: () -> ExpectChain,
	never_exists: () -> ExpectChain,

	-- Equality checks
	is: (expected: any) -> ExpectChain,
	never_is: (expected: any) -> ExpectChain,

	-- Boolean checks
	is_true: () -> ExpectChain,
	never_is_true: () -> ExpectChain,

	-- Type checks
	is_a: (typeName: string) -> ExpectChain,
	never_is_a: (typeName: string) -> ExpectChain,

	-- Object/table property checks
	has_key: (key: any) -> ExpectChain,
	never_has_key: (key: any) -> ExpectChain,
	has_value: (value: any) -> ExpectChain,
	never_has_value: (value: any) -> ExpectChain,

	-- String checks
	starts_with: (prefix: string) -> ExpectChain,
	never_starts_with: (prefix: string) -> ExpectChain,
	ends_with: (suffix: string) -> ExpectChain,
	never_ends_with: (suffix: string) -> ExpectChain,
	includes: (substring: string) -> ExpectChain,
	never_includes: (substring: string) -> ExpectChain,

	-- Snapshot testing
	matches_snapshot: (key: string?) -> ExpectChain,

	-- Error checks (function failure testing)
	fails: () -> ExpectChain,
	never_fails: () -> ExpectChain,
	fails_with: (expectedMessage: string) -> ExpectChain,
	never_fails_with: (expectedMessage: string) -> ExpectChain,
}

-- Test framework function signatures
export type DescribeFunction = (label: string, testSuite: () -> ()) -> ()
export type TestFunction = (label: string, testImplementation: () -> ()) -> ()
export type ExpectFunction = (actual: any) -> ExpectChain

-- Configuration types for tiniest_for_lute
export type TiniestOptions = {
	snapshot_path: string?,
	save_snapshots: boolean?,
	pretty: {
		disable_colour: boolean?,
		disable_emoji: boolean?,
		disable_unicode: boolean?,
		disable_output: {
			after_run: boolean?,
		}?,
	}?,
}

-- Plugin system types
export type Plugin = {
	notify: (event: string, ...any) -> (),
}

export type FrameworkOptions = {
	plugins: { Plugin }?,
}

export type RunOptions = {}

-- Main framework interface returned by configure()
export type TiniestFramework = {
	describe: DescribeFunction,
	test: TestFunction,
	collect_tests: (testSuite: () -> ()) -> { Test },
	run_tests: (tests: { Test }, options: RunOptions) -> RunResult,
}

-- Configured instance with expect function
export type TiniestForLuteFramework = {
	describe: DescribeFunction,
	test: TestFunction,
	expect: {
		expect: ExpectFunction,
	},
	collect_tests: (testSuite: () -> ()) -> { Test },
	run_tests: (tests: { Test }, options: RunOptions) -> RunResult,
	save_snapshots: () -> (),
}

return nil


--!strict

--[=[
	@class RecommendedRules

	Default recommended rules for luau-lint

	This plugin contains the core set of rules that are recommended for
	most Luau projects. These rules enforce best practices and help
	catch common issues.

	@since v0.1.0
]=]

local GeneralizedIteration = require("./rules/generalized-iteration")

-- Export all rules in this plugin
return {
	rules = {
		GeneralizedIteration,
	},

	-- Plugin metadata
	name = "recommended-rules",
	version = "0.1.0",
	description = "Default recommended rules for luau-lint",
}


#!/usr/bin/env lute
--!strict

-- Simple test runner script
local tiniest_module = require("./packages/test-utils/src/tiniest/tiniest_for_lute")
local fs = require("@lute/fs")

-- Configure tiniest
local tiniest = tiniest_module.configure({
	snapshot_path = "./tests/__snapshots__",
	save_snapshots = true,
})

-- List of test files to run from the new package-based structure
local testFiles = {
	"./tests/packages/cli/args.spec",
	"./tests/packages/cli/output.spec",
	"./tests/packages/core/error-handling.spec",
	"./tests/packages/plugin-api/registry.spec",
	"./tests/packages/plugin-api/rule.spec",
	"./tests/packages/plugin-api/plugin-api.spec",
	"./tests/packages/plugin-api/context.spec",
}

print("Running test suite...")
print("===================")

-- Function to run all test suites
local function runAllTests()
	local describe = tiniest.describe
	local test = tiniest.test
	local expect = tiniest.expect

	-- Add a simple test to verify the framework works
	describe("Test Framework", function()
		test("basic arithmetic", function()
			expect(2 + 2).is(4)
		end)

		test("string concatenation", function()
			expect("hello" .. " " .. "world").is("hello world")
		end)
	end)

	-- Load and run each test file
	for _, testFile in testFiles do
		local success, testSuite = pcall(require, testFile)

		if success and type(testSuite) == "function" then
			print(`Loading test suite: {testFile}`)
			testSuite(describe, test, expect)
		else
			print(`Failed to load test suite {testFile}: {testSuite}`)
		end
	end
end

-- Collect and run all tests
local tests = tiniest.collect_tests(runAllTests)
local results = tiniest.run_tests(tests, {})

-- Print results summary
print("\nTest Results:")
print("=============")
print(`Total tests: {#tests}`)
print(`Passed: {results.status_tally.pass}`)
print(`Failed: {results.status_tally.fail}`)

if results.status_tally.fail > 0 then
	print("\nFailed tests:")
	for test, result in results.individual do
		if result.status == "fail" then
			print(`  - {table.concat(test.labels, " > ")}`)
			print(`    Error: {result.error.message}`)
		end
	end
	-- Exit with error code
	error("Tests failed", 0)
else
	print("\nAll tests passed! ✅")
end


#!/usr/bin/env lune

--[[
    Clean Directories Script for luau-lint

    This script recursively removes all directories with specified names
    throughout the project. Useful for cleaning up generated files like
    luau_packages, node_modules, .pesde, etc.

    Usage:
        lune run scripts/clean-directories.luau
        lune run scripts/clean-directories.luau -- --dirs luau_packages .pesde
        lune run scripts/clean-directories.luau -- --dry-run
        lune run scripts/clean-directories.luau -- --help

    Examples:
        # Clean default directories (luau_packages)
        lune run scripts/clean-directories.luau

        # Clean specific directories
        lune run scripts/clean-directories.luau -- --dirs luau_packages .pesde node_modules

        # Preview what would be deleted without actually deleting
        lune run scripts/clean-directories.luau -- --dry-run
]]

local fs = require("@lune/fs")
local process = require("@lune/process")
local stdio = require("@lune/stdio")

-- Configuration
local DEFAULT_CLEAN_DIRS = {
	"luau_packages",
	".pesde",
}

local EXCLUDED_PATHS = {
	".git",
	"target",
	"dist",
	"build",
}

-- ANSI color codes for pretty output
local colors = {
	reset = "\27[0m",
	green = "\27[32m",
	blue = "\27[34m",
	yellow = "\27[33m",
	red = "\27[31m",
	gray = "\27[90m",
	bold = "\27[1m",
}

local function log(color: string, prefix: string, message: string)
	print(`{color}{colors.bold}{prefix}{colors.reset} {message}`)
end

local function logInfo(message: string)
	log(colors.blue, "ℹ", message)
end

local function logSuccess(message: string)
	log(colors.green, "✅", message)
end

local function logWarning(message: string)
	log(colors.yellow, "⚠", message)
end

local function logError(message: string)
	log(colors.red, "❌", message)
end

local function logDebug(message: string)
	log(colors.gray, "🔍", message)
end

local function showHelp()
	print("Clean Directories Script")
	print("Usage: lune run scripts/clean-directories.luau [OPTIONS]")
	print("")
	print("Options:")
	print("  --dirs <names...>    Specify directory names to clean (space-separated)")
	print("  --dry-run           Show what would be deleted without actually deleting")
	print("  --force             Skip confirmation prompt (use with caution)")
	print("  --help              Show this help message")
	print("")
	print("Examples:")
	print("  lune run scripts/clean-directories.luau")
	print("  lune run scripts/clean-directories.luau -- --dirs luau_packages .pesde")
	print("  lune run scripts/clean-directories.luau -- --dry-run")
	print("  lune run scripts/clean-directories.luau -- --force")
	print("")
	print(`Default directories to clean: {table.concat(DEFAULT_CLEAN_DIRS, ", ")}`)
end

-- Parse command line arguments
local function parseArgs(): ({ string }, boolean, boolean)
	local args = process.args
	local dirsToClean = DEFAULT_CLEAN_DIRS
	local dryRun = false
	local force = false

	local i = 1
	while i <= #args do
		local arg = args[i]

		if arg == "--help" or arg == "-h" then
			showHelp()
			process.exit(0)
		elseif arg == "--dry-run" then
			dryRun = true
		elseif arg == "--force" then
			force = true
		elseif arg == "--dirs" then
			-- Collect all directory names after --dirs
			dirsToClean = {}
			i += 1
			while i <= #args and string.sub(args[i], 1, 2) ~= "--" do
				table.insert(dirsToClean, args[i])
				i += 1
			end
			i -= 1 -- Adjust for the outer loop increment
		end

		i += 1
	end

	return dirsToClean, dryRun, force
end

-- Check if a path should be excluded from traversal
local function isExcludedPath(path: string): boolean
	for _, excluded in EXCLUDED_PATHS do
		if string.find(path, excluded, 1, true) then
			return true
		end
	end
	return false
end

-- Recursively find all directories with specified names
local function findDirectories(rootPath: string, targetNames: { string }): { string }
	local found = {}

	local function traverse(currentPath: string)
		-- Skip excluded paths
		if isExcludedPath(currentPath) then
			return
		end

		-- Check if current path exists and is a directory
		if not fs.isDir(currentPath) then
			return
		end

		local entries = fs.readDir(currentPath)

		for _, entry in entries do
			local fullPath = `{currentPath}/{entry}`

			-- Check if this entry is one of our target directories
			local isTargetDir = false
			for _, targetName in targetNames do
				if entry == targetName and fs.isDir(fullPath) then
					table.insert(found, fullPath)
					-- Don't traverse into directories we're going to delete
					isTargetDir = true
					break
				end
			end

			-- Recursively traverse subdirectories (but not target directories)
			if not isTargetDir and fs.isDir(fullPath) and not isExcludedPath(fullPath) then
				traverse(fullPath)
			end
		end
	end

	traverse(rootPath)
	return found
end

-- Get human-readable size of a directory
local function getDirectorySize(path: string): string
	-- Simple size estimation by counting files
	local function countFiles(dirPath: string): number
		if not fs.isDir(dirPath) then
			return 0
		end

		local count = 0
		for entry in fs.readDir(dirPath) do
			local fullPath = `{dirPath}/{entry}`
			if fs.isFile(fullPath) then
				count += 1
			elseif fs.isDir(fullPath) then
				count += countFiles(fullPath)
			end
		end
		return count
	end

	local success, fileCount = pcall(countFiles, path)
	if not success or fileCount == 0 then
		return "empty"
	elseif fileCount < 10 then
		return `{fileCount} files`
	elseif fileCount < 100 then
		return `~{math.floor(fileCount / 10) * 10} files`
	else
		return `~{math.floor(fileCount / 100) * 100}+ files`
	end
end

-- Remove a directory and all its contents
local function removeDirectory(path: string): boolean
	local success, err = pcall(function()
		fs.removeDir(path)
		return nil
	end)

	if not success then
		logError(`Failed to remove {path}: {err}`)
		return false
	end

	return true
end

-- Main script logic
local function main()
	local dirsToClean, dryRun, force = parseArgs()

	if dryRun then
		logWarning("DRY RUN MODE - No files will actually be deleted")
	end

	logInfo(`Searching for directories: {table.concat(dirsToClean, ", ")}`)

	-- Find all matching directories
	local foundDirs = findDirectories(".", dirsToClean)

	if #foundDirs == 0 then
		logInfo("No directories found to clean")
		return
	end

	logInfo(`Found {#foundDirs} director{#foundDirs > 1 and "ies" or "y"}:`)

	-- Show what will be deleted
	for _, dirPath in foundDirs do
		local size = getDirectorySize(dirPath)
		if dryRun then
			logDebug(`Would delete: {dirPath} ({size})`)
		else
			logDebug(`Will delete: {dirPath} ({size})`)
		end
	end

	if dryRun then
		logInfo("Dry run complete. Use without --dry-run to actually delete directories.")
		return
	end

	-- Confirm deletion for safety
	local confirmed = force

	if not force then
		local success, result = pcall(
			stdio.prompt,
			"confirm",
			`About to delete {#foundDirs} director{#foundDirs > 1 and "ies" or "y"}. Continue?`
		)

		if success then
			confirmed = result
		else
			-- Fallback for non-interactive environments
			logWarning(
				"Running in non-interactive environment. Use --force to proceed automatically."
			)
			logInfo("Operation cancelled")
			return
		end

		if not confirmed then
			logInfo("Operation cancelled")
			return
		end
	else
		logWarning("Force mode enabled - skipping confirmation")
	end

	-- Delete directories
	local deletedCount = 0
	local failedCount = 0

	for _, dirPath in foundDirs do
		logInfo(`Deleting {dirPath}...`)

		if removeDirectory(dirPath) then
			logSuccess(`Deleted {dirPath}`)
			deletedCount += 1
		else
			failedCount += 1
		end
	end

	-- Summary
	if failedCount == 0 then
		logSuccess(
			`Successfully deleted {deletedCount} director{deletedCount > 1 and "ies" or "y"}`
		)
	else
		logWarning(`Deleted {deletedCount}, failed {failedCount}`)
	end

	if deletedCount > 0 then
		logInfo("Clean up complete! You may want to run 'pesde install' to reinstall dependencies.")
	end
end

-- Run the script
main()


#!/usr/bin/env lune

--[[
    Copy License Script for luau-lint

    This script copies the root LICENSE file to each package directory
    in the packages/ folder to ensure all publishable packages have
    the proper licensing information.

    Usage: lune run scripts/copy-license.luau
]]

local fs = require("@lune/fs")
local process = require("@lune/process")

-- Configuration
local ROOT_LICENSE_PATH = "LICENSE"
local PACKAGES_DIR = "packages"

-- ANSI color codes for pretty output
local colors = {
    reset = "\27[0m",
    green = "\27[32m",
    blue = "\27[34m",
    yellow = "\27[33m",
    red = "\27[31m",
    bold = "\27[1m",
}

local function log(color: string, prefix: string, message: string)
    print(`{color}{colors.bold}{prefix}{colors.reset} {message}`)
end

local function logInfo(message: string)
    log(colors.blue, "ℹ", message)
end

local function logSuccess(message: string)
    log(colors.green, "✅", message)
end

local function logWarning(message: string)
    log(colors.yellow, "⚠", message)
end

local function logError(message: string)
    log(colors.red, "❌", message)
end

-- Main script logic
local function main()
    logInfo("Starting license copy process...")

    -- Check if root LICENSE exists
    if not fs.isFile(ROOT_LICENSE_PATH) then
        logError(`Root LICENSE file not found at: {ROOT_LICENSE_PATH}`)
        process.exit(1)
    end

    -- Check if packages directory exists
    if not fs.isDir(PACKAGES_DIR) then
        logError(`Packages directory not found at: {PACKAGES_DIR}`)
        process.exit(1)
    end

    -- Read the root LICENSE content
    local licenseContent = fs.readFile(ROOT_LICENSE_PATH)
    logInfo(`Read LICENSE file ({#licenseContent} characters)`)

    -- Get all package directories
    local packageDirs = {}
    local entries = fs.readDir(PACKAGES_DIR)
    for _, entry in entries do
        local packagePath = `{PACKAGES_DIR}/{entry}`
        if fs.isDir(packagePath) then
            table.insert(packageDirs, entry)
        end
    end

    if #packageDirs == 0 then
        logWarning("No package directories found")
        return
    end

    logInfo(`Found {#packageDirs} package(s): {table.concat(packageDirs, ", ")}`)

    -- Copy LICENSE to each package
    local copiedCount = 0
    local skippedCount = 0

    for _, packageName in packageDirs do
        local packagePath = `{PACKAGES_DIR}/{packageName}`
        local packageLicensePath = `{packagePath}/LICENSE`

        -- Check if package LICENSE already exists and is identical
        if fs.isFile(packageLicensePath) then
            local existingContent = fs.readFile(packageLicensePath)
            if existingContent == licenseContent then
                logInfo(`{packageName}: LICENSE already up to date`)
                skippedCount += 1
                continue
            end
        end

        -- Copy the LICENSE
        fs.writeFile(packageLicensePath, licenseContent)
        logSuccess(`{packageName}: LICENSE copied`)
        copiedCount += 1
    end

    -- Summary
    logInfo(`Process complete! Copied: {copiedCount}, Skipped: {skippedCount}, Total: {#packageDirs}`)

    if copiedCount > 0 then
        logSuccess("All packages now have the correct LICENSE file")
    end
end

-- Run the script
main()


-- Auto-generated by dphfox/tiniest. Do not modify!
--!nocheck
return {[====[{
  ["bar"] = 2;
  ["foo"] = true;
  ["hello"] = "world";
}]====]}


-- Example .luau-lint.luau configuration file for testing
return {
	rules = {
		["unused-variable"] = "warn",
		["undefined-global"] = "error",
	},
	ignorePatterns = { "*.generated.luau" },
	severity = "warn",
}


-- Modern Luau code using generalized iteration
local array = { 1, 2, 3, 4, 5 }

-- Modern Luau: Direct iteration over values (no ipairs needed)
for i, value in ipairs(array) do
	print(value)
end

-- -- Modern Luau: Still direct iteration (no ipairs needed)
for index, item in ipairs(array) do
	print("Processing:", item)
end

-- -- Modern Luau: Direct iteration with unused index
for _, element in pairs(array) do
	print(element)
end

-- -- Additional examples of modern Luau iteration:

-- -- Dictionary/map iteration (no pairs needed)
local userData = { name = "Alice", age = 30, city = "Tokyo" }
for key, value in pairs(userData) do
	print(key .. ":", value)
end

-- -- Mixed table iteration (Luau handles both array and hash parts)
local mixedTable = { 10, 20, x = 100, y = 200 }
for k, v in pairs(mixedTable) do
	print(k, v)
end

-- -- Nested table iteration
local matrix = { { 1, 2 }, { 3, 4 }, { 5, 6 } }
for i, row in ipairs(matrix) do
	for j, value in ipairs(row) do
		print("matrix[" .. i .. "][" .. j .. "] =", value)
	end
end


-- Before and After: Luau Generalized Iteration Examples

-- ❌ BEFORE: Legacy approach with ipairs/pairs
--[[
local items = {"apple", "banana", "cherry"}

-- Old way - unnecessary ipairs wrapper
for i, item in ipairs(items) do
    print(i, item)
end

-- Old way - unnecessary pairs wrapper
local settings = {theme = "dark", volume = 50}
for key, value in pairs(settings) do
    print(key .. ":", value)
end
--]]

-- ✅ AFTER: Modern Luau generalized iteration

local items = { "apple", "banana", "cherry" }

-- Modern way - direct iteration (Luau 5.1+)
for i, item in items do
	print(i, item)
end

-- When you only need values
for _, item in items do
	print("Processing:", item)
end

-- When you need specific numeric control
for i = 1, #items do
	print("Item at position", i, "is", items[i])
end

-- Dictionary/map iteration
local settings = { theme = "dark", volume = 50 }
for key, value in settings do
	print(key .. ":", value)
end

-- Mixed tables work seamlessly
local mixedData = { 100, 200, name = "test", active = true }
for k, v in mixedData do
	print(k, "->", v)
end

-- Nested structures
local gameData = {
	players = {
		{ name = "Alice", score = 150 },
		{ name = "Bob", score = 120 },
	},
	settings = {
		difficulty = "hard",
		sound = true,
	},
}

-- Iterate players
for i, player in gameData.players do
	print("Player", i .. ":", player.name, "Score:", player.score)
end

-- Iterate settings
for setting, value in gameData.settings do
	print("Game setting", setting .. ":", value)
end

--[[
BENEFITS OF MODERN LUAU ITERATION:
1. Cleaner syntax - no wrapper functions
2. Better performance - direct iteration
3. Unified approach - same syntax for arrays and dictionaries
4. Less typing - shorter code
5. Better type inference in TypeScript/Luau
--]]


-- Modern Luau code demonstrating efficient iteration patterns
local array = { 1, 2, 3, 4, 5 }

-- When you need numeric indices specifically, use numeric for loop
for i = 1, #array do
	print("Index:", i, "Value:", array[i])
end

-- Modern Luau: Direct iteration over key-value pairs (replaces ipairs)
for i, value in array do
	print("Index:", i, "Value:", value)
end

-- When you only need values (most common case)
for _, value in array do
	print("Processing:", value)
end

-- Dictionary iteration (replaces pairs)
local settings = {
	volume = 80,
	brightness = 100,
	theme = "dark",
}

for key, value in settings do
	print(key .. " =", value)
end

-- Complex iteration examples
local students = {
	{ name = "Alice", grade = 95 },
	{ name = "Bob", grade = 87 },
	{ name = "Charlie", grade = 92 },
}

-- Iterate over student records
for index, student in students do
	print("Student #" .. index .. ":", student.name, "Grade:", student.grade)
end

-- Iterate over student properties
for _, student in students do
	for property, value in student do
		print("  " .. property .. ":", value)
	end
end


-- Test file with modern Luau iteration that should pass linting
local data = { 10, 20, 30, 40, 50 }

-- Modern Luau: No ipairs needed
for i, value in data do
	print("Item", i, ":", value)
end

-- Modern Luau: Dictionary iteration without pairs
local config = { debug = true, timeout = 5000 }
for key, val in config do
	print(key, "=", val)
end

-- Legacy code that would be flagged (commented out)
-- for i, v in ipairs(data) do
--     print(v)  -- This would trigger our linter
-- end


--!strict

local fs = require("@lute/fs")
local tiniest = require("@tiniest/tiniest_for_lute").configure({
	snapshot_path = "./tests/__snapshots__",
	save_snapshots = true,
})

-- Function to recursively find all .spec.luau files
local function findTestFiles(directory: string): { string }
	local testFiles = {}

	local function walkDirectory(dir: string)
		local entries = fs.listdir(dir)

		for _, entry in entries do
			local fullPath = dir .. "/" .. entry.name

			if
				entry.type == "dir"
				and entry.name ~= "__snapshots__"
				and entry.name ~= "tiniest"
			then
				-- Recursively search subdirectories (skip snapshots and tiniest)
				walkDirectory(fullPath)
			elseif
				entry.type == "file"
				and string.match(entry.name, "%.spec%.luau$")
				and entry.name ~= "test.spec.luau"
			then
				-- Found a test file (exclude the main test runner)
				table.insert(testFiles, fullPath)
			end
		end
	end

	walkDirectory(directory)
	return testFiles
end

-- Function to convert file path to require path
local function pathToRequire(filePath: string): string
	local requirePath = string.gsub(filePath, "^%./tests/", "./")
	requirePath = string.gsub(requirePath, "%.luau$", "")
	return requirePath
end

local function runAllTests()
	local describe = tiniest.describe
	local test = tiniest.test
	local expect = tiniest.expect

	-- Example test to verify runner works
	describe("Test Runner", function()
		test("can run basic tests", function()
			expect(2 + 2).is(4)
		end)

		test("can discover test files", function()
			local tests = findTestFiles("./tests")
			expect(#tests).is_a("number")
			expect(#tests).never_is(0)
		end)
	end)

	-- Dynamically discover and run all test files
	local testFiles = findTestFiles("./tests")

	for _, filePath in testFiles do
		-- Skip the problematic integration test file for now

		local requirePath = pathToRequire(filePath)
		local success, testSuite = pcall(require, requirePath)

		if success and type(testSuite) == "function" then
			-- Run the test suite
			testSuite(describe, test, expect)
		else
			print(`Failed to load test suite from {requirePath}: {testSuite}`)
		end
	end

	-- Run the fixed integration tests
	local success, fixedIntegrationTests =
		pcall(require, "integration/integration-test-scenarios-fixed")
	if success and type(fixedIntegrationTests) == "function" then
		fixedIntegrationTests(describe, test, expect)
	else
		print(`Failed to load fixed integration tests: {fixedIntegrationTests}`)
	end
end

local tests = tiniest.collect_tests(runAllTests)
tiniest.run_tests(tests, {})


--> Run stylua to check for formatting errors

local process = require("@lune/process")

local CommandBuilder = require("./lib/exec")

process.exit(
	CommandBuilder.new("stylua")
		:withArg(".")
		:withArgs(process.args)
		:withStdioStrategy("forward")
		:exec().code
)


--- An MPSC synchronization primitive powered by Lua upvalues which retains only
--- one value at a time.

--- ## Usage
--- ```luau
--- local send, recv = watch((nil :: any) :: string)
--- task.delay(5, send, "hello, world!")
--- task.spawn(function()
---     local value = recv()
---     print("received value:", value)
--- end)
--- ```
type Watch<T> = {
	value: T?,
	receivers: { thread },
}

--- Creates a new `Watch` channel, returning its send and receive handles.
local function chan<T>(_phantom: T): ((T) -> (), () -> T?)
	local watch: Watch<T> = {
		value = nil,
		receivers = {},
	}

	local function send(value: T)
		watch.value = value

		for _, receiver in watch.receivers do
			coroutine.resume(receiver, value)
		end
	end

	local function recv(): T
		local value = watch.value
		watch.value = nil

		if value == nil then
			table.insert(watch.receivers, coroutine.running())
			return coroutine.yield()
		end

		return value :: T
	end

	return send, recv
end

return chan


--> lib: Builder pattern class to spawn child processes

local process = require("@lune/process")
local stdio = require("@lune/stdio")

local CommandBuilder = {}

export type CommandBuilder = typeof(setmetatable(
	{} :: CommandBuilderFields,
	{ __index = CommandBuilder }
))
type CommandBuilderFields = {
	program: string,
	args: { string },
	stdioStrategy: IoStrategyMapping?,
}
export type StdioStrategy = "pipe" | "inherit" | "forward" | "none"
export type IoStrategyMapping = {
	stdin: StdioStrategy?,
	stdout: StdioStrategy?,
	stderr: StdioStrategy?,
}

local DEFAULT_STDIO_STRATEGY: IoStrategyMapping = {
	stdin = "pipe",
	stdout = "pipe",
	stderr = "pipe",
}
function CommandBuilder.new(program: string)
	return setmetatable(
		{
			program = program,
			args = {},
			stdioStrategy = nil,
		} :: CommandBuilderFields,
		{
			__index = CommandBuilder,
		}
	)
end

function CommandBuilder.withArg(
	self: CommandBuilder,
	arg: string
): CommandBuilder
	table.insert(self.args, arg)
	return self
end

function CommandBuilder.withArgs(
	self: CommandBuilder,
	args: { string }
): CommandBuilder
	for _, arg in args do
		self:withArg(arg)
	end

	return self
end

function CommandBuilder.withStdioStrategy(
	self: CommandBuilder,
	strategy: StdioStrategy | IoStrategyMapping
): CommandBuilder
	self.stdioStrategy = if typeof(strategy) == "string"
		then {
			stdout = strategy,
			stderr = strategy,
		}
		else strategy
	return self
end

local function intoSpawnOptionsStdioKind(
	strategy: StdioStrategy
): process.ExecStdioKind
	if strategy == "pipe" then
		return "default"
	end

	if strategy == "inherit" then
		return "inherit"
	end

	if strategy == "forward" then
		return "forward"
	end

	if strategy == "none" then
		return "none"
	end

	error(`Non-strategy provided: {strategy}`)
end

function CommandBuilder.exec(self: CommandBuilder): process.ExecResult
	print(
		"$",
		stdio.style("dim") .. self.program,
		table.concat(self.args, " ") .. stdio.style("reset")
	)

	local function translateIoStrategyMappings(mappings: IoStrategyMapping)
		local translatedMappings: process.ExecStdioOptions = {}
		for field, value in mappings do
			translatedMappings[field] =
				intoSpawnOptionsStdioKind(value :: StdioStrategy)
		end

		return translatedMappings
	end

	local child = process.exec(self.program, self.args, {
		shell = true,
		stdio = translateIoStrategyMappings(
			self.stdioStrategy or DEFAULT_STDIO_STRATEGY
		),
	})

	if not child.ok then
		print(
			`\n{stdio.color("red")}[luau-lsp]{stdio.color("reset")} Exited with code`,
			child.code
		)
	end

	return child
end

return CommandBuilder


local fs = require("@lune/fs")
local serde = require("@lune/serde")

export type SPDXLicense =
	"MIT"
	| "Apache-2.0"
	| "BSD-2-Clause"
	| "BSD-3-Clause"
	| "GPL-2.0"
	| "GPL-3.0"
	| "LGPL-2.1"
	| "LGPL-3.0"
	| "MPL-2.0"
	| "ISC"
	| "Unlicense"
	| "WTFPL"
	| "Zlib"
	| "CC0-1.0"
	| "CC-BY-4.0"
	| "CC-BY-SA-4.0"
	| "BSL-1.0"
	| "EPL-2.0"
	| "AGPL-3.0"

export type DependencySpecifier = (({
	name: string,
	version: string,
	index: string?,
} | { workspace: string, version: string }) & {
	target: string?,
}) | {
	wally: string,
	version: string,
	index: string?,
} | {
	repo: string,
	rev: string,
	path: string?,
}

export type PackageTarget = {
	environment: "roblox" | "roblox_server",
	lib: string,
} | ({
	environment: "luau" | "lune",
} & (
	{ scripts: { [string]: string } } | { lib: string } | { bin: string }
))

export type PesdeManifest<T = {}> = {
	name: string,
	version: string,
	description: string?,
	license: SPDXLicense?,
	authors: { string }?,
	repository: string?,
	private: boolean?,
	includes: { string }?,
	pesde_version: string?,
	workspace_members: { string }?,
	target: PackageTarget,
	build_files: { string }?,
	scripts: { [string]: string }?,
	indices: { [string]: string },
	wally_indices: { [string]: string }?,
	overrides: { [string]: DependencySpecifier }?,
	patches: { [string]: { [string]: string } }?,
	place: { [string]: string }?,
	dependencies: { [string]: DependencySpecifier }?,
	peer_dependencies: { [string]: DependencySpecifier }?,
	dev_dependencies: { [string]: DependencySpecifier }?,
} & T

return function<T>(path: string?, _phantom: T): PesdeManifest<T>
	local manifestContents = fs.readFile(path or "pesde.toml")
	local decoded = serde.decode("toml", manifestContents)

	return decoded :: PesdeManifest<T>
end


--> Run tests using frktest runner

local fs = require("@lune/fs")
local process = require("@lune/process")

local frktest = require("../luau_packages/frktest")
local reporter = require("@self/reporter")

-- HACK: Cast require to allow for dynamic paths in strict mode
-- A more proper solution would be to use luau.load instead, but
-- frktest requires its global state to be modified by test suites
local require = require :: (
	path: string
) -> (
	test: typeof(setmetatable(
		{} :: {
			case: (name: string, fn: () -> nil) -> (),
			suite: (name: string, fn: () -> ()) -> (),
		},
		{ __index = frktest.test }
	))
) -> ()

local function discoverTests(dir: string): { string }
	local tests = {}

	local entries = fs.readDir(dir)
	for _, entry in entries do
		local path = `{dir}/{entry}`

		-- Look for files ending in `.spec.luau` as tests
		if fs.isFile(path) and string.match(entry, "%.spec%.luau$") then
			table.insert(tests, path)
			continue
		end

		-- Recurse for directories
		if fs.isDir(path) then
			local dirResults = discoverTests(path)
			table.move(dirResults, 1, #dirResults, #tests + 1, tests)
			continue
		end
	end

	return tests
end

local allowedTests = process.args
for _, test in discoverTests("src") do
	-- If we are given any arguments, we only run those tests, otherwise,
	-- we run all the tests

	-- So, to include only a certain set of test files, you can provide either
	-- the full path of the test file or name of the test file, with or without
	-- the `.spec.luau` extension
	local baseName = string.match(test, "([^/\\]+)$") :: string
	local baseNameWithoutExt = string.gsub(baseName, "%.luau$", "")
	local testPath = string.gsub(test, "%.luau$", "")
	local isAllowed = #process.args == 0
		or table.find(allowedTests, test)
		or table.find(allowedTests, testPath)
		or table.find(allowedTests, baseName)
		or table.find(allowedTests, baseNameWithoutExt)

	local constructors = {
		case = frktest.test.case,
		suite = frktest.test.suite,
	}

	if not isAllowed then
		constructors.case = frktest.test.skip.case
		constructors.suite = frktest.test.skip.suite
	end

	require(`../{testPath}`)(
		setmetatable(constructors, { __index = frktest.test })
	)
end

reporter.init()
process.exit(tonumber(frktest.run()))


--> Run luau-lsp analysis to check for type errors

local process = require("@lune/process")

local CommandBuilder = require("./lib/exec")

process.exit(
	CommandBuilder.new("luau-lsp")
		:withArg("analyze")
		:withArgs({ "--settings", ".vscode/settings.json" })
		:withArgs({ "--ignore", "'**/.pesde/**'" })
		:withArgs({ "--ignore", "'./test-files/**'" })
		:withArg(".")
		:withStdioStrategy("forward")
		:exec().code
)


local rojoSyncConfig = require("../rojo/sync_config")

export type TreeProperties = rojoSyncConfig.TreeProperties
export type TreeBase = rojoSyncConfig.TreeBase
export type TreeNormal = rojoSyncConfig.TreeNormal
export type TreeService = rojoSyncConfig.TreeService
export type DataModelTree = rojoSyncConfig.DataModelTree
export type Tree = rojoSyncConfig.Tree
export type SyncConfig = rojoSyncConfig.SyncConfig

_G.interactive = false

--- Generates an Argon sync configuration file (`default.project.json`) from a list of
--- input files to be included.

--- ## Errors
--- * The current process lacks permissions to a file
--- * Any I/O error occurs
return function(
	packageDirectory: string?,
	files: { string },
	options: {
		writeToFile: boolean?,
		force: boolean?,
	}
): (boolean, string?)
	-- Purely alias to the Rojo sync config generator, since Argon uses the same
	-- config format
	return rojoSyncConfig(packageDirectory, files, options)
end


local fs = require("@lune/fs")
local process = require("@lune/process")
local serde = require("@lune/serde")

export type TreeProperties = {
	Name: never?,
	Parent: never?,
}

export type TreeBase = {
	["$className"]: string?,
	["$ignoreUnknownInstances"]: boolean?,
	["$path"]: string | { optional: string }?,
	["$properties"]: TreeProperties?,
}

export type TreeNormal = TreeBase & {
	[string]: TreeNormal,
} & ({ ["$className"]: string } | {
	["$path"]: string | { optional: string },
})

export type TreeService = TreeBase & {
	[string]: TreeNormal,
}

export type DataModelTree = TreeBase & {
	StarterPlayer: (TreeBase & {
		StarterPlayerScripts: TreeService?,
		StarterCharacterScripts: TreeService?,
		[string]: TreeNormal,
	})?,
	[string]: TreeService,
}

export type Tree =
	(DataModelTree & {
		["$className"]: "DataModel",
	})
	| TreeNormal

export type SyncConfig = {
	name: string,
	servePort: number?,
	servePlaceIds: { number }?,
	placeId: number?,
	gameId: number?,
	serveAddress: string?,
	globIgnorePaths: { string }?,
	tree: Tree,
}

local PLATFORM_SEP = if process.os == "windows" then "\\" else "/"
_G.interactive = false

--- Generates a Rojo sync configuration file (`default.project.json`) from a list of
--- input files to be included.

--- ## Errors
--- * The current process lacks permissions to a file
--- * Any I/O error occurs
return function(
	packageDirectory: string?,
	files: { string },
	options: {
		writeToFile: boolean?,
		force: boolean?,
	}
): (boolean, string?)
	packageDirectory = packageDirectory or process.cwd
	local syncConfigPath =
		`{packageDirectory}{PLATFORM_SEP}default.project.json`
	if fs.isFile(syncConfigPath) and not options.force then
		return true, nil
	end

	local syncConfigTree = {} :: Tree

	for _, file in files do
		-- Remove the `.lua` or `.luau` file extension from the file name
		local name = string.gsub(file, ".luau?$", "")

		if name == "init" then
			syncConfigTree["$path"] = name
			continue
		end

		syncConfigTree[name] = {
			["$path"] = file,
		}
	end

	-- If there isn't a top level path, we mark the entire thing as a Folder
	if not syncConfigTree["$path"] then
		syncConfigTree["$className"] = "Folder"
	end

	-- If the config tree does not include pesde's downloaded roblox dependencies
	-- directory, we add it as an optional one for the future, once dependencies
	-- are installed
	if not syncConfigTree["roblox_packages"] then
		syncConfigTree["roblox_packages"] = {
			["$path"] = {
				optional = "roblox_packages",
			},
		}
	end

	-- If the config tree does not include pesde's downloaded roblox server dependencies
	-- directory, we add it as an optional one for the future, once server dependencies
	-- are installed
	if not syncConfigTree["roblox_server_packages"] then
		syncConfigTree["roblox_server_packages"] = {
			["$path"] = {
				optional = "roblox_server_packages",
			},
		}
	end

	-- Finally, we serialize the config to a JSON string and optionally write it
	-- to the sync config path
	local serializedConfig = serde.encode("json", {
		name = string.match(packageDirectory :: string, "[^\\/]+$"),
		tree = syncConfigTree,
	}, true)
	if options.writeToFile then
		fs.writeFile(syncConfigPath, serializedConfig)
	end

	return true, serializedConfig
end


return {
	generators = {
		rojo = {
			sourcemap = require("@self/generators/rojo/sourcemap"),
			syncConfig = require("@self/generators/rojo/sync_config"),
		},

		argon = {
			sourcemap = require("@self/generators/argon/sourcemap"),
			syncConfig = require("@self/generators/argon/sync_config"),
		},
	},
}


-- scripts/build.luau

local process = require("@lune/process")
local multitarget = require("../lune_packages/multitarget")

local result = multitarget.build(
    "pesde.toml",
    {
        luau = true;
        lune = true;
        roblox = true;
    },
    "dist",
    {
        "src";
    },
    "LUA_ENV"
)

if result:isOk() then
    print("Built successfully!")

    local result = process.exec("rojo", { "build", "rbxm.project.json", "--output", "release.rbxm" })

    if result.ok then
        print(result.stdout)
    else
        print("Rojo Error:", result.stderr)
    end
else
    print("Error:", result:unwrapErr().message)
end


--#[ Head Variables ]#--
local module = {}

--#[ Mdules ]#--
local super_table = require("@luau_pkg/super_table")
local hangul = require("../internal/hangul")
local disassemble = require("../core/disassemble")

--#[ Variables ]#--
--

--#[ Functions ]#--
--

--#[ Main ]#--
function module.assemble(
    fragments: { string }
): string
    local disassembled = disassemble.disassemble(table.concat(fragments))
    local graphemes: { string } = {}

    for _, charCode in utf8.codes(disassembled) do
        local character = utf8.char(charCode)

        table.insert(graphemes, character)
    end

    return super_table.reduce(
        graphemes,
        function(
            accumulator: string,
            index: number,
            fragment: string
        )
            return hangul.binaryAssemble(accumulator, fragment)
        end,
        ""
    )
end

return module


--#[ Head Variables ]#--
local module = {}

--#[ Modules ]#--
local constants = require("../internal/constants")

--#[ Variables ]#--
local canBeChoseongCache: { [string]: boolean } = {}
local canBeJungseongCache: { [string]: boolean } = {}
local canBeJongseongCache: { [string]: boolean } = {}

--#[ Functions ]#--
--

--#[ Main ]#--
function module.canBeChoseong(
    character: string
): boolean
    if canBeChoseongCache[character] then
        return true
    end

    if table.find(constants.CHOSEONGS, character) then
        canBeChoseongCache[character] = true

        return true
    else
        return false
    end
end

function module.canBeJungseong(
    character: string
): boolean
    if canBeJungseongCache[character] then
        return true
    end

    if constants.DISASSEMBLE_VOWELS_BY_VOWEL[character] then
        canBeJungseongCache[character] = true

        return true
    end

    if table.find(constants.JUNGSEONGS, character) then
        canBeJungseongCache[character] = true

        return true
    else
        return false
    end
end

function module.canBeJongseong(
    character: string
): boolean
    if canBeJongseongCache[character] then
        return true
    end

    if table.find(constants.JONGSEONGS, character) then
        canBeJongseongCache[character] = true

        return true
    else
        return false
    end
end

return module


--#[ Head Variables ]#--
local module = {}

--#[ Mdules ]#--
local constants = require("../internal/constants")

--#[ Variables ]#--
--

--#[ Functions ]#--
--

--#[ Main ]#--
function module.combineCharacter(
    choseong: string,
    jungseong: string,
    jongseong: string?
): string
    local choseongIndex = table.find(constants.CHOSEONGS, choseong)

    if not choseongIndex then
        error("Invalid choseong: " .. choseong)
    end

    local jungseongIndex = table.find(constants.JUNGSEONGS, jungseong)

    if not jungseongIndex then
        error("Invalid jungseong: " .. jungseong)
    end

    local jongseongIndex = 1

    if jongseong then
        jongseongIndex = table.find(constants.JONGSEONGS, jongseong) or 0

        if jongseongIndex == 0 then
            error("Invalid jongseong: " .. jongseong)
        end
    end

    local numberOfJungseongs = #constants.JUNGSEONGS
    local numberOfJongseongs = #constants.JONGSEONGS

    local choseongOfTargetConsonant = (choseongIndex - 1)
        * numberOfJungseongs
        * numberOfJongseongs
    local choseongOfTargetVowel = (jungseongIndex - 1)
        * numberOfJongseongs

    local unicode = constants.COMPLETE_HANGUL_START_CHARCODE
        + choseongOfTargetConsonant
        + choseongOfTargetVowel
        + jongseongIndex - 1

    return utf8.char(unicode)
end

function module.combineVowels(
    vowel1: string,
    vowel2: string
): string
    for combinedVowel, value in next, constants.DISASSEMBLE_VOWELS_BY_VOWEL, nil do
        if value == `{ vowel1 }{ vowel2 }` then
            return combinedVowel
        end
    end

    return `{ vowel1 }{ vowel2 }`
end

return module


--#[ Types ]#--
export type DisassembledCompleteCharacter = {
    choseong: string;
    jungseong: string;
    jongseong: string;
}

--#[ Head Variables ]#--
local module = {}

--#[ Modules ]#--
local super_table = require("@luau_pkg/super_table")
local constants = require("../internal/constants")

--#[ Variables ]#--
--

--#[ Functions ]#--
local function splitHangulCharacters(
    string: string
): { string }
    local splited = {}

    for _, charCode in utf8.codes(string) do
        table.insert(splited, utf8.char(charCode))
    end

    return splited
end

--#[ Main ]#--
function module.disassembleCompleteCharacter(
    character: string
): DisassembledCompleteCharacter?
    local charCode = utf8.codepoint(character)
    local isCompleteHangul = constants.COMPLETE_HANGUL_START_CHARCODE <= charCode and charCode <= constants.COMPLETE_HANGUL_END_CHARCODE

    if not isCompleteHangul then
        return nil
    end

    local hangulCode = charCode - constants.COMPLETE_HANGUL_START_CHARCODE

    local jongseongIndex = hangulCode % constants.NUMBER_OF_JONGSEONG
    local jungseongIndex = ((hangulCode - jongseongIndex) // constants.NUMBER_OF_JONGSEONG) % constants.NUMBER_OF_JUNGSEONG
    local choseongIndex = math.floor((hangulCode - jongseongIndex) // constants.NUMBER_OF_JONGSEONG // constants.NUMBER_OF_JUNGSEONG)

    --// Luau는 index가 1부터 시작하기 때문에 1씩 더해준다.
    return {
        choseong = constants.CHOSEONGS[choseongIndex + 1];
        jungseong = constants.JUNGSEONGS[jungseongIndex + 1];
        jongseong = constants.JONGSEONGS[jongseongIndex + 1];
    }
end

function module.disassembleToGroups(
    string: string
): { { string } }
    local result: { { string } } = {}

    for _, charCode in utf8.codes(string) do
        local character = utf8.char(charCode)
        local group: { string } = {}
        local disassembledCompleteCharacter = module.disassembleCompleteCharacter(character)

        if disassembledCompleteCharacter ~= nil then
            table.insert(group, disassembledCompleteCharacter.choseong)

            for _, character in next, splitHangulCharacters(disassembledCompleteCharacter.jungseong), nil do
                table.insert(group, character)
            end

            for _, character in next, splitHangulCharacters(disassembledCompleteCharacter.jongseong), nil do
                table.insert(group, character)
            end

            table.insert(result, group)

            continue
        end

        if constants.DISASSEMBLE_CONSONANTS_BY_CONSONANT[character] then
            for _, character in next, splitHangulCharacters(constants.DISASSEMBLE_CONSONANTS_BY_CONSONANT[character]), nil do
                table.insert(group, character)
            end

            table.insert(result, group)

            continue
        end

        if constants.DISASSEMBLE_VOWELS_BY_VOWEL[character] then
            for _, character in next, splitHangulCharacters(constants.DISASSEMBLE_VOWELS_BY_VOWEL[character]), nil do
                table.insert(group, character)
            end

            table.insert(result, group)

            continue
        end

        table.insert(result, { character })
    end

    return result
end

function module.disassemble(
    string: string
): string
    return super_table.reduce(
        module.disassembleToGroups(string),
        function(
            accumulator: string,
            index: number,
            disassembleds: { string }
        )
            return `{ accumulator }{ table.concat(disassembleds) }`
        end,
        ""
    )
end

return module


--[[
    Luau에는 normalize관련 기능이 없기 때문에 다른 방식으로 초성을 구한다.
]]

--#[ Head Variables ]#--
local module = {}

--#[ Mdules ]#--
local super_table = require("@luau_pkg/super_table")
local disassemble = require("../core/disassemble")

--#[ Variables ]#--
--

--#[ Functions ]#--
--

--#[ Main ]#--
function module.getChoseong(
    word: string
): string
    return super_table.reduce(
        disassemble.disassembleToGroups(word),
        function(
            accumulator: string,
            key: number,
            value: { string }
        )
            local choseong = value[1]

            return `{ accumulator }{ choseong }`
        end,
        ""
    )
end

return module


--#[ Head Variables ]#--
local module = {}

--#[ Modules ]#--
local constants = require("../internal/constants")

--#[ Variables ]#--
--

--#[ Functions ]#--
--

--#[ Main ]#--
function module.hasBatchim(
    string: string
): boolean
    if utf8.len(string) == 0 then
        return false
    end

    local charCode = utf8.codepoint(
        string,
        utf8.offset(string, -1)
    )

    if charCode < constants.COMPLETE_HANGUL_START_CHARCODE or charCode > constants.COMPLETE_HANGUL_END_CHARCODE then
        return false
    end

    local batchimIndex = (charCode - constants.COMPLETE_HANGUL_START_CHARCODE) % constants.NUMBER_OF_JONGSEONG + 1

    return batchimIndex > 1
end

function module.hasSingleBatchim(
    string: string
): boolean
    if utf8.len(string) == 0 then
        return false
    end

    local charCode = utf8.codepoint(
        string,
        utf8.offset(string, -1)
    )

    if charCode < constants.COMPLETE_HANGUL_START_CHARCODE or charCode > constants.COMPLETE_HANGUL_END_CHARCODE then
        return false
    end

    local batchimIndex = (charCode - constants.COMPLETE_HANGUL_START_CHARCODE) % constants.NUMBER_OF_JONGSEONG + 1

    if batchimIndex <= 0 then
        return false
    end

    local batchimLength = utf8.len(constants.JONGSEONGS[batchimIndex])

    return batchimLength == 1
end

function module.hasDoubleBatchim(
    string: string
): boolean
    if utf8.len(string) == 0 then
        return false
    end

    local charCode = utf8.codepoint(
        string,
        utf8.offset(string, -1)
    )

    if charCode < constants.COMPLETE_HANGUL_START_CHARCODE or charCode > constants.COMPLETE_HANGUL_END_CHARCODE then
        return false
    end

    local batchimIndex = (charCode - constants.COMPLETE_HANGUL_START_CHARCODE) % constants.NUMBER_OF_JONGSEONG + 1

    if batchimIndex <= 0 then
        return false
    end

    local batchimLength = utf8.len(constants.JONGSEONGS[batchimIndex])

    return batchimLength == 2
end

return module


local assemble = require("@self/assemble")
local canBe = require("@self/canBe")
local combine = require("@self/combine")
local disassemble = require("@self/disassemble")
local getChoseong = require("@self/getChoseong")
local hasBatchim = require("@self/hasBatchim")
local josa = require("@self/josa")
local removeLastCharacter = require("@self/removeLastCharacter")

return {
    assemble = assemble;
    canBe = canBe;
    combine = combine;
    disassemble = disassemble;
    getChoseong = getChoseong;
    hasBatchim = hasBatchim;
    josa = josa;
    removeLastCharacter = removeLastCharacter;
}


--#[ Types ]#--
type JosaOption = "이/가"
    | "을/를"
    | "은/는"
    | "으로/로"
    | "와/과"
    | "이나/나"
    | "이란/란"
    | "아/야"
    | "이랑/랑"
    | "이에요/예요"
    | "으로서/로서"
    | "으로써/로써"
    | "으로부터/로부터"
    | "이라/라"

--#[ Head Variables ]#--
local module = {}

--#[ Modules ]#--
local disassemble = require("../core/disassemble")
local hasBatchim = require("../core/hasBatchim")
local constants = require("../internal/constants")

--#[ Variables ]#--
local roJosa: { JosaOption } = {
    "으로/로";
    "으로서/로서";
    "으로써/로써";
    "으로부터/로부터";
}

--#[ Functions ]#--
--

--#[ Main ]#--
function module.pick(
    word: string,
    josa: JosaOption
): string
    if utf8.len(word) == 0 then
        return string.split(josa, "/")[1]
    end

    local hasWordBatchim = hasBatchim.hasBatchim(word)
    local index = hasWordBatchim and 1 or 2

    local lastOffset = utf8.offset(word, -1) :: number
    local lastCharacter = string.sub(word, lastOffset, #word)
    local disassembledCompleteCharacter = disassemble.disassembleCompleteCharacter(lastCharacter)
    local isJongseong = hasWordBatchim
        and disassembledCompleteCharacter
        and disassembledCompleteCharacter.jongseong == "ㄹ"

    local isCaseOfRo = hasWordBatchim
        and isJongseong
        and table.find(roJosa, josa)

    if josa == "와/과" or isCaseOfRo then
        index = index == 1
            and 2
            or 1
    end

    return string.split(josa, "/")[index]
end

function module.josa(
    word: string,
    josa: JosaOption
): string
    if utf8.len(word) == 0 then
        return word
    end

    if string.match(word, "^[A-Z]+$") then
        local lastOffset = utf8.offset(word, -1) :: number
        local lastCharacter = string.sub(word, lastOffset, #word)
        local koreanPronunciationOfLastCharacter = constants.ALPHABET_TO_KOREAN[lastCharacter]

        return `{ word }{ module.pick(koreanPronunciationOfLastCharacter, josa) }`
    end

    return `{ word }{ module.pick(word, josa) }`
end

return module


--#[ Head Variables ]#--
local module = {}

--#[ Mdules ]#--
local canBe = require("../core/canBe")
local combine = require("../core/combine")
local disassemble = require("../core/disassemble")

--#[ Variables ]#--
--

--#[ Functions ]#--
local function excludeLastElement<V>(
    array: { V }
): ({ V }, V)
    local lastElement = array[#array]
    local newArray = {}

    for index = 1, #array - 1 do
        local value = array[index]

        newArray[index] = value
    end

    return newArray, lastElement
end

--#[ Main ]#--
function module.removeLastCharacter(
    words: string
): string
    if utf8.len(words) == 0 then
        return ""
    end

    local lastCharacter = utf8.char(utf8.codepoint(words, utf8.offset(words, -1)))

    local result: string do
        local disassembledLastCharacter = disassemble.disassembleToGroups(lastCharacter)
        local lastCharacterWithoutLastAlphabet = excludeLastElement(disassembledLastCharacter[1])

        if #lastCharacterWithoutLastAlphabet <= 3 then
            local first, middle, last = table.unpack(lastCharacterWithoutLastAlphabet)

            if middle ~= nil then
                result = canBe.canBeJungseong(last)
                    and combine.combineCharacter(first, `{ middle }{ last }`)
                    or combine.combineCharacter(first, middle, last)
            else
                result = first or ""
            end
        else
            local first, firstJunseong, secondJungseong, firstJongseong = table.unpack(lastCharacterWithoutLastAlphabet)

            result = combine.combineCharacter(first, `{ firstJunseong }{ secondJungseong }`, firstJongseong)
        end
    end

    local lastCharacterOffset = utf8.offset(words, -1)
    local wordsWithoutLastCharacter = lastCharacterOffset ~= nil
        and string.sub(words, 0, lastCharacterOffset - 1)
        or ""

    return `{ wordsWithoutLastCharacter }{ result }`
end

return module


local core = require("@self/core")
local number = require("@self/number")

return {
    core = core;
    number = number;
}


--#[ Modules ]#--
local super_table = require("@luau_pkg/super_table")

--#[ Variables ]#--
local COMPLETE_HANGUL_START_CHARCODE = utf8.codepoint("가")
local COMPLETE_HANGUL_END_CHARCODE = utf8.codepoint("힣")
local COMPLETE_HANGUL_ALPHABET_START_CHARCODE = utf8.codepoint("ㄱ")
local COMPLETE_HANGUL_ALPHABET_END_CHARCODE = utf8.codepoint("ㅣ")

local NUMBER_OF_JONGSEONG = 28;
local NUMBER_OF_JUNGSEONG = 21;

--// 종성이 없는 경우 '빈' 초성으로 관리하는 것이 편리하여, 빈 문자열도 포함한다.
local DISASSEMBLE_CONSONANTS_BY_CONSONANT = {
    [""] = "";
    ["ㄱ"] = "ㄱ";
    ["ㄲ"] = "ㄲ";
    ["ㄳ"] = "ㄱㅅ";
    ["ㄴ"] = "ㄴ";
    ["ㄵ"] = "ㄴㅈ";
    ["ㄶ"] = "ㄴㅎ";
    ["ㄷ"] = "ㄷ";
    ["ㄸ"] = "ㄸ";
    ["ㄹ"] = "ㄹ";
    ["ㄺ"] = "ㄹㄱ";
    ["ㄻ"] = "ㄹㅁ";
    ["ㄼ"] = "ㄹㅂ";
    ["ㄽ"] = "ㄹㅅ";
    ["ㄾ"] = "ㄹㅌ";
    ["ㄿ"] = "ㄹㅍ";
    ["ㅀ"] = "ㄹㅎ";
    ["ㅁ"] = "ㅁ";
    ["ㅂ"] = "ㅂ";
    ["ㅃ"] = "ㅃ";
    ["ㅄ"] = "ㅂㅅ";
    ["ㅅ"] = "ㅅ";
    ["ㅆ"] = "ㅆ";
    ["ㅇ"] = "ㅇ";
    ["ㅈ"] = "ㅈ";
    ["ㅉ"] = "ㅉ";
    ["ㅊ"] = "ㅊ";
    ["ㅋ"] = "ㅋ";
    ["ㅌ"] = "ㅌ";
    ["ㅍ"] = "ㅍ";
    ["ㅎ"] = "ㅎ";
};

local DISASSEMBLE_VOWELS_BY_VOWEL = {
    ["ㅏ"] = "ㅏ";
    ["ㅐ"] = "ㅐ";
    ["ㅑ"] = "ㅑ";
    ["ㅒ"] = "ㅒ";
    ["ㅓ"] = "ㅓ";
    ["ㅔ"] = "ㅔ";
    ["ㅕ"] = "ㅕ";
    ["ㅖ"] = "ㅖ";
    ["ㅗ"] = "ㅗ";
    ["ㅘ"] = "ㅗㅏ";
    ["ㅙ"] = "ㅗㅐ";
    ["ㅚ"] = "ㅗㅣ";
    ["ㅛ"] = "ㅛ";
    ["ㅜ"] = "ㅜ";
    ["ㅝ"] = "ㅜㅓ";
    ["ㅞ"] = "ㅜㅔ";
    ["ㅟ"] = "ㅜㅣ";
    ["ㅠ"] = "ㅠ";
    ["ㅡ"] = "ㅡ";
    ["ㅢ"] = "ㅡㅣ";
    ["ㅣ"] = "ㅣ";
};

local CHOSEONGS = {
    "ㄱ";
    "ㄲ";
    "ㄴ";
    "ㄷ";
    "ㄸ";
    "ㄹ";
    "ㅁ";
    "ㅂ";
    "ㅃ";
    "ㅅ";
    "ㅆ";
    "ㅇ";
    "ㅈ";
    "ㅉ";
    "ㅊ";
    "ㅋ";
    "ㅌ";
    "ㅍ";
    "ㅎ";
}

local JUNGSEONGS = super_table.values(DISASSEMBLE_VOWELS_BY_VOWEL)
table.sort(
    JUNGSEONGS,
    function(
        a: string,
        b: string
    )
        return a < b
    end
)

--// 종성으로 올 수 있는 자모에는 ㄸ, ㅃ, ㅉ이 없기 때문에 따로 적는다.
local JONGSEONGS = super_table.map(
    {
        "";
        "ㄱ";
        "ㄲ";
        "ㄳ";
        "ㄴ";
        "ㄵ";
        "ㄶ";
        "ㄷ";
        "ㄹ";
        "ㄺ";
        "ㄻ";
        "ㄼ";
        "ㄽ";
        "ㄾ";
        "ㄿ";
        "ㅀ";
        "ㅁ";
        "ㅂ";
        "ㅄ";
        "ㅅ";
        "ㅆ";
        "ㅇ";
        "ㅈ";
        "ㅊ";
        "ㅋ";
        "ㅌ";
        "ㅍ";
        "ㅎ";
    },
    function(
        _,
        consonant
    )
        return DISASSEMBLE_CONSONANTS_BY_CONSONANT[consonant] :: string;
    end
)

local HANGUL_DIGITS = {
    "만";
    "억";
    "조";
    "경";
    "해";
    "자";
    "양";
    "구";
    "간";
    "정";
    "재";
    "극";
    "항하사";
    "아승기";
    "나유타";
    "불가사의";
    "무량대수";
    "겁";
    "업";
}

local HANGUL_DIGITS_MAX = #HANGUL_DIGITS * 4
local HANGUL_NUMBERS = { "일", "이", "삼", "사", "오", "육", "칠", "팔", "구" }
local HANGUL_NUMBERS_FOR_DECIMAL = { "영", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구" }
local HANGUL_CARDINAL = { "십", "백", "천" }

local ALPHABET_TO_KOREAN = {
    ["A"] = "에이";
    ["B"] = "비";
    ["C"] = "씨";
    ["D"] = "디";
    ["E"] = "이";
    ["F"] = "에프";
    ["G"] = "지";
    ["H"] = "에이치";
    ["I"] = "아이";
    ["J"] = "제이";
    ["K"] = "케이";
    ["L"] = "엘";
    ["M"] = "엠";
    ["N"] = "엔";
    ["O"] = "오";
    ["P"] = "피";
    ["Q"] = "큐";
    ["R"] = "알";
    ["S"] = "에스";
    ["T"] = "티";
    ["U"] = "유";
    ["V"] = "브이";
    ["W"] = "더블유";
    ["X"] = "엑스";
    ["Y"] = "와이";
    ["Z"] = "지";
}

return {
    COMPLETE_HANGUL_START_CHARCODE = COMPLETE_HANGUL_START_CHARCODE;
    COMPLETE_HANGUL_END_CHARCODE = COMPLETE_HANGUL_END_CHARCODE;
    COMPLETE_HANGUL_ALPHABET_START_CHARCODE = COMPLETE_HANGUL_ALPHABET_START_CHARCODE;
    COMPLETE_HANGUL_ALPHABET_END_CHARCODE = COMPLETE_HANGUL_ALPHABET_END_CHARCODE;

    NUMBER_OF_JONGSEONG = NUMBER_OF_JONGSEONG;
    NUMBER_OF_JUNGSEONG = NUMBER_OF_JUNGSEONG;

    DISASSEMBLE_CONSONANTS_BY_CONSONANT = DISASSEMBLE_CONSONANTS_BY_CONSONANT;
    DISASSEMBLE_VOWELS_BY_VOWEL = DISASSEMBLE_VOWELS_BY_VOWEL;

    CHOSEONGS = CHOSEONGS;
    JUNGSEONGS = JUNGSEONGS;
    JONGSEONGS = JONGSEONGS;

    HANGUL_DIGITS = HANGUL_DIGITS;
    HANGUL_DIGITS_MAX = HANGUL_DIGITS_MAX;
    HANGUL_NUMBERS = HANGUL_NUMBERS;
    HANGUL_NUMBERS_FOR_DECIMAL = HANGUL_NUMBERS_FOR_DECIMAL;
    HANGUL_CARDINAL = HANGUL_CARDINAL;

    ALPHABET_TO_KOREAN = ALPHABET_TO_KOREAN;
}


--#[ Head Variables ]#--
local module = {}

--#[ Variables ]#--
local DAYS_MAP = {
    [1] = "하루";
    [2] = "이틀";
    [3] = "사흘";
    [4] = "나흘";
    [5] = "닷새";
    [6] = "엿새";
    [7] = "이레";
    [8] = "여드레";
    [9] = "아흐레";
    [10] = "열";
    [20] = "스무";
}

local DAYS_ONLY_TENS_MAP = {
    [10] = "열흘";
    [20] = "스무날";
    [30] = "서른날";
}

--#[ Functions ]#--
local function validateNumber(
    number: number
)
    if
        number ~= number
        or number <= 0
        or number > 30
        or number % 1 ~= 0
        or number >= math.huge
    then
        error("Number must be an integer between 1 and 30.")
    end
end

local function getNumberWord(
    number: number
): string
    validateNumber(number)

    local tens = math.floor(number / 10) * 10
    local ones = number % 10

    if ones == 0 and DAYS_ONLY_TENS_MAP[tens] then
        return DAYS_ONLY_TENS_MAP[tens]
    end

    local tensWord = DAYS_MAP[tens] or ""
    local onesWord = DAYS_MAP[ones]

    return tensWord .. onesWord
end

--#[ Main ]#--
function module.days(
    number: number
): string
    return getNumberWord(number)
end

return module


local days = require("@self/days")
local numberToHangul = require("@self/numberToHangul")
local susa = require("@self/susa")

return {
    days = days;
    numberToHangul = numberToHangul;
    susa = susa;
}


--#[ Head Variables ]#--
local module = {}

--#[ Modules ]#--
local super_table = require("@luau_pkg/super_table")
local constants = require("../internal/constants")

--#[ Variables ]#--
--

--#[ Functions ]#--
local function formatNumber(
    number: number
): string
    local sign, integer, decimal = string.match(tostring(number), "^([%-%+]?)(%d+)([.]?%d*)$")

    if not integer then
        return tostring(number)
    end

    local reversed = string.reverse(integer)
    reversed = string.gsub(reversed, "(%d%d%d)", "%1,")
    integer = string.gsub(string.reverse(reversed), "^,", "")

    sign = sign or ""
    sign = sign == "+" and "" or sign

    return (sign :: string) .. integer .. (decimal or "")
end

local function numberToKoreanUpToThousand(
    number: number
): string
    local resultParts: { string } = {}

    for index, digit in next, string.split(string.reverse(tostring(number)), "") do
        local resultPart: string

        if digit == "0" then
            continue
        end

        if index - 1 > 0 and digit == "1" then
            resultPart = constants.HANGUL_CARDINAL[index - 1]
        else
            resultPart = constants.HANGUL_NUMBERS[tonumber(digit) :: number]
                .. (constants.HANGUL_CARDINAL[index - 1] or "")
        end

        table.insert(
            resultParts,
            resultPart
        )
    end

    local result = ""

    for i = #resultParts, 1, -1 do
        result ..= resultParts[i]
    end

    return result
end

local function numberToHangul(
    number: number,
    withSpace: boolean?
): string
    if type(number) ~= "number" or number ~= number then
        error("Input must be a valid number.")
    end

    if number == 0 then
        return "영"
    end

    if number == math.huge then
        return "무한대"
    end

    if number == -math.huge then
        return withSpace
            and "마이너스 무한대"
            or "마이너스무한대"
    end

    local isNegative = number < 0
    local absoluteNumber = math.abs(number)

    local integerPart, decimalPart = table.unpack(string.split(tostring(absoluteNumber), "."))

    local koreanParts: { string } = {}
    local remainingDigits = integerPart
    local placeIndex = 0;

    while #remainingDigits > 0 do
        local currentPart = string.sub(remainingDigits, -4)

        local koreanNumber = numberToKoreanUpToThousand(tonumber(currentPart) :: number)

        if koreanNumber ~= "" then
            table.insert(
                koreanParts,
                1,
                `{ koreanNumber }{ constants.HANGUL_DIGITS[placeIndex] or "" }`
            )
        end

        remainingDigits = string.sub(remainingDigits, 1, -5)
        placeIndex += 1
    end

    local result: string
    if integerPart == "0" then
        result = "영"
    else
        result = table.concat(
            koreanParts,
            withSpace
                and " "
                or ""
        )
    end

    if decimalPart then
        local decimalKorean = super_table.reduce(
            string.split(decimalPart, ""),
            function(
                accumulator: string,
                _,
                digit: string
            ): string
                return accumulator
                    .. constants.HANGUL_NUMBERS_FOR_DECIMAL[(tonumber(digit) :: number) + 1]
            end,
            ""
        )

        result ..= `점{
            withSpace
                and " "
                or ""
        }{ decimalKorean }`
    end

    if isNegative then
        result = `마이너스{
            withSpace
                and " "
                or ""
        }{ result }`
    end

    return result
end

local function numberToHangulMixed(
    number: number,
    withSpace: boolean?
): string
    if type(number) ~= "number" or number ~= number then
        error("Input must be a valid number.")
    end

    if number == 0 then
        return "0"
    end

    if number == math.huge then
        return "무한대"
    end

    if number == -math.huge then
        return "-무한대"
    end

    local isNegative = number < 0
    local absoluteNumber = math.abs(number)

    local integerPart, decimalPart = table.unpack(string.split(tostring(absoluteNumber), "."))

    local koreanParts: { string } = {}
    local remainingDigits = integerPart
    local placeIndex = 0;

    while #remainingDigits > 0 do
        local currentPart = string.sub(remainingDigits, -4)
        local numericValue = tonumber(currentPart) :: number

        if numericValue > 0 then
            local formattedPart = `{ formatNumber(numericValue) }{ constants.HANGUL_DIGITS[placeIndex] or "" }`

            table.insert(
                koreanParts,
                1,
                formattedPart
            )
        end

        remainingDigits = string.sub(remainingDigits, 1, -5)
        placeIndex += 1
    end

    local result: string
    if integerPart == "0" then
        result = "0"
    else
        result = table.concat(
            koreanParts,
            withSpace
                and " "
                or ""
        )
    end

    if decimalPart then
        result ..= `.{ decimalPart }`
    end

    if isNegative then
        result = `-{
            withSpace
                and " "
                or ""
        }{ result }`
    end

    return result
end

--#[ Main ]#--
function module.numberToHangul(
    number: number
): string
    return numberToHangul(number)
end

function module.numberToHangulWithSpace(
    number: number
): string
    return numberToHangul(number, true)
end

function module.numberToHangulMixed(
    number: number
): string
    return numberToHangulMixed(number)
end

function module.numberToHangulMixedWithSpace(
    number: number
): string
    return numberToHangulMixed(number, true)
end

return module


--#[ Head Variables ]#--
local module = {}

--#[ Modules ]#--
local numberToHangul = require("./numberToHangul")

--#[ Variables ]#--
local SUSA_MAP = {
    [1] = "하나";
    [2] = "둘";
    [3] = "셋";
    [4] = "넷";
    [5] = "다섯";
    [6] = "여섯";
    [7] = "일곱";
    [8] = "여덟";
    [9] = "아홉";
    [10] = "열";
    [20] = "스물";
    [30] = "서른";
    [40] = "마흔";
    [50] = "쉰";
    [60] = "예순";
    [70] = "일흔";
    [80] = "여든";
    [90] = "아흔";
    [100] = "백";
}

local SUSA_CLASSIFIER_MAP = {
    [1] = "한";
    [2] = "두";
    [3] = "세";
    [4] = "네";
    [20] = "스무";
}

local SEOSUSA_MAP = {
    [1] = "한";
    [2] = "두";
    [3] = "셋";
    [4] = "넷";
    [5] = "다섯";
    [6] = "여섯";
    [7] = "일곱";
    [8] = "여덟";
    [9] = "아홉";
    [10] = "열";
    [20] = "스물";
    [30] = "서른";
    [40] = "마흔";
    [50] = "쉰";
    [60] = "예순";
    [70] = "일흔";
    [80] = "여든";
    [90] = "아흔";
    [100] = "백";
}

local SEOSUSA_SPECIAL_CASE_MAP = {
  [1] = "첫";
  [2] = "둘";
  [20] = "스무";
}

--#[ Functions ]#--
local function validateSusa(
    number: number
)
    if
        number ~= number
        or number <= 0
        or number > 100
        or number % 1 ~= 0
        or number >= math.huge
    then
        error("Number must be an integer between 1 and 30.")
    end
end

local function validateSeosusa(
    number: number
)
    if
        number ~= number
        or number < 1
        or number % 1 ~= 0
        or number >= math.huge
    then
        error("Number must be an integer greater than or equal to 1.")
    end
end

function getOrdinalWord(
    number: number
): string
    if SEOSUSA_SPECIAL_CASE_MAP[number] then
        return SEOSUSA_SPECIAL_CASE_MAP[number]
    end

    local tens = math.floor(number / 10) * 10
    local ones = number % 10

    local tensWord = SEOSUSA_MAP[tens] or ""
    local onesWord = SEOSUSA_MAP[ones] or ""

    return `{ tensWord }{ onesWord }`
end

--#[ Main ]#--
function module.susa(
    number: number
): string
    validateSusa(number)

    if number == 100 then
        return SUSA_MAP[100]
    end

    local tens = math.floor(number / 10) * 10
    local ones = number % 10

    local tensWord = SUSA_MAP[tens] or ""
    local onesWord = SUSA_MAP[ones] or ""

    return `{ tensWord }{ onesWord }`
end

function module.susaClassifier(
    number: number
): string
    validateSusa(number)

    if number == 20 then
        return SUSA_CLASSIFIER_MAP[20]
    end

    local tens = math.floor(number / 10) * 10
    local ones = number % 10

    local tensWord = SUSA_MAP[tens] or ""

    if ones == 0 then
        return tensWord
    end

    if SUSA_CLASSIFIER_MAP[ones] then
        return `{ tensWord }{ SUSA_CLASSIFIER_MAP[ones] }`
    else
        local onesWord = SUSA_MAP[ones] or ""
        return `{ tensWord }{ onesWord }`
    end
end

function module.seosusa(
    number: number
): string
    validateSeosusa(number)

    if number >= 1 and number <= 99 then
        return `{ getOrdinalWord(number) }째`
    else
        return `{ numberToHangul.numberToHangul(number) }째`
    end
end

return module


local pathfs = require("../src/lib")

print(pathfs.normalize("../src/./lib/../lib/normalizePath.luau"))
print(pathfs.canonicalize("./.vscode"))


local pathfs = require("../src/lib")
local task = require("@lune/task")

local disconnectObserver = pathfs.observeDescendantEntry(".playground/test", function(entry)
	print("Hello bro", entry)
	return function()
		print("Oh no I didn't")
	end
end)

task.wait(5)

disconnectObserver()


local pathfs = require("../src/lib")

-- Example data to serialize
local config = {
	name = "Example Application",
	version = "1.0.0",
	database = {
		host = "localhost",
		port = 5432,
		name = "myapp"
	},
	features = {
		"authentication",
		"logging",
		"caching"
	},
	settings = {
		debug = true,
		maxConnections = 100
	}
}

print("=== pathfs Serialization Example ===\n")

-- Serialize to different formats
print("1. Serializing to JSON...")
pathfs.serializeFile(config, "example-config.json")
print("   ✓ Saved to example-config.json")

print("\n2. Serializing to TOML...")
pathfs.serializeFile(config, "example-config.toml")
print("   ✓ Saved to example-config.toml")

print("\n3. Serializing to YAML...")
pathfs.serializeFile(config, "example-config.yaml")
print("   ✓ Saved to example-config.yaml")

-- Deserialize and verify
print("\n4. Deserializing JSON...")
local jsonConfig = pathfs.deserializeFile("example-config.json")
print(`   ✓ Loaded: {jsonConfig.name} v{jsonConfig.version}`)
print(`   ✓ Database: {jsonConfig.database.host}:{jsonConfig.database.port}`)

print("\n5. Deserializing TOML...")
local tomlConfig = pathfs.deserializeFile("example-config.toml")
print(`   ✓ Loaded: {tomlConfig.name} v{tomlConfig.version}`)
print(`   ✓ Features: {table.concat(tomlConfig.features, ", ")}`)

print("\n6. Deserializing YAML...")
local yamlConfig = pathfs.deserializeFile("example-config.yaml")
print(`   ✓ Loaded: {yamlConfig.name} v{yamlConfig.version}`)
print(`   ✓ Debug mode: {yamlConfig.settings.debug}`)

-- Works with Path objects too
print("\n7. Using Path objects...")
local configPath = pathfs.Path.from("path-object-config.json")
pathfs.serializeFile({ message = "Hello from Path object!" }, configPath)
local pathObjectConfig = pathfs.deserializeFile(configPath)
print(`   ✓ Message: {pathObjectConfig.message}`)

-- Clean up example files
print("\n8. Cleaning up...")
pathfs.fs.removeFile("example-config.json")
pathfs.fs.removeFile("example-config.toml")
pathfs.fs.removeFile("example-config.yaml")
pathfs.fs.removeFile("path-object-config.json")
print("   ✓ Cleaned up example files")

print("\n=== Example completed successfully! ===")


local pathfs = require("../src/lib")

local file = pathfs.FilePath.new("hi"):withFileWritten("hello")
file:writeFile("hallo")
file:removeFile()

local dir = pathfs.DirectoryPath.new("hi"):withDirWritten()
dir:removeDir()

pathfs.writeFileAll(pathfs.Path.new("a/b/c"):join(file.path), "hello2")


local pathfs = require("../src/lib")

pathfs.watchDirectories({
	".playground/test",
	".playground/test2",
}, function()
	print("yolo")
end)


local function loadTests()
	require("@tests/path")()
	require("@tests/fs")()
	require("@tests/utilities")()
end

local function runTests()
	local frktest = require("../luau_packages/frktest")

	frktest.reporters.console_reporter.init()
	local success = frktest.run()
	if not success then
		require("@lune/process").exit(1)
	end
end

loadTests()
runTests()


local luauPath = require("../luau_packages/luau_path")
local Path = luauPath.Path
local fs = require("./fs")
local types = require("./types")

local PathType = types.PathType
local AsPathType = types.AsPathType
local optionalBooleanType = types.optionalBooleanType
local arrayOfStringType = types.arrayOfStringType
local booleanType = types.booleanType

export type DirectoryPath = typeof(setmetatable(
	{} :: {
		path: typeof(PathType:type()),
	},
	{} :: DirectoryPathImpl
))

type DirectoryPathImpl = {
	__index: DirectoryPathImpl,
	__tostring: (self: DirectoryPath) -> string,
	new: (dirPath: typeof(AsPathType:type())) -> DirectoryPath,
	fromExisting: (dirPath: typeof(AsPathType:type())) -> DirectoryPath,
	withDirWritten: (self: DirectoryPath, allowOverwrite: typeof(optionalBooleanType:type())) -> DirectoryPath,
	is: (value: any) -> boolean,
	isDir: (self: DirectoryPath) -> typeof(booleanType:type()),
	readDir: (self: DirectoryPath) -> typeof(arrayOfStringType:type()),
	writeDir: (self: DirectoryPath) -> (),
	removeDir: (self: DirectoryPath) -> (),
}

--[=[
	@class DirectoryPath

	Represents a directory path.

	### Example
	```lua
	local dirPath = DirectoryPath.fromExisting("path/to/dir")
	dirPath:removeDir()
	```
]=]
local DirectoryPath = {} :: DirectoryPathImpl
DirectoryPath.__index = DirectoryPath
--[=[
	@within DirectoryPath
	@prop path Path
	The underlying `Path` instance.
]=]

--[=[
	@within DirectoryPath

	Returns a string representation of the `DirectoryPath` instance.

	### Example
	```lua
	local dirPath = DirectoryPath.fromExisting("path/to/dir")
	print(dirPath) -- Output: DirectoryPath<path/to/dir>
	```

	@param self DirectoryPath -- The DirectoryPath instance
	@return string -- A string representation of the DirectoryPath instance
]=]
function DirectoryPath.__tostring(self)
	assert(PathType(self.path))

	return `DirectoryPath<{self.path}>`
end

--[=[
	@within DirectoryPath

	Creates a new `DirectoryPath` instance.

	@param dirPath AsPath -- The directory path
	@return DirectoryPath -- A new `DirectoryPath` instance
]=]
function DirectoryPath.new(dirPath)
	assert(AsPathType(dirPath))

	return setmetatable({
		path = Path.from(dirPath),
	}, DirectoryPath)
end

--[=[
	@within DirectoryPath

	Creates a new `DirectoryPath` instance from an existing directory.

	An error will be thrown if the directory does not exist.

	@param dirPath AsPath -- The directory path
	@return DirectoryPath -- A new `DirectoryPath` instance
]=]
function DirectoryPath.fromExisting(dirPath)
	assert(AsPathType(dirPath))

	if not fs.isDir(dirPath) then
		error(`There is no directory at that path({dirPath})`)
	end

	return setmetatable({
		path = Path.from(dirPath),
	}, DirectoryPath)
end

--[=[
	@within DirectoryPath

	Writes the directory to the filesystem if it does not already exist.

	An error will be thrown in the following situations:

	* A file already exists at the directory path.
	* The current process lacks permissions to write the directory.
	* Some other I/O error occurred.

	### Example
	```lua
	local dirPath = DirectoryPath.new("path/to/dir"):withDirWritten()
	```

	@param self DirectoryPath -- The DirectoryPath instance
	@param allowOverwrite boolean? -- Whether to allow overwriting an existing directory. Defaults to `false`.
	@return DirectoryPath -- The `DirectoryPath` instance
]=]
function DirectoryPath.withDirWritten(self, allowOverwrite)
	assert(optionalBooleanType(allowOverwrite))

	if self:isDir() then
		if allowOverwrite then
			self:removeDir()
		else
			error(`Not allowed to overwrite a directory. A directory already exists at that path({self.path})`)
		end
	end
	if fs.isFile(self.path) then
		error(`Cannot write a directory. The file already exists at that path({self.path})`)
	end
	self:writeDir()

	return self
end

--[=[
	@within DirectoryPath

	Checks if a value is a `DirectoryPath` instance.

	@param value any -- The value to check
	@return boolean -- Whether the value is a `DirectoryPath` instance
]=]
function DirectoryPath.is(value)
	return type(value) == "table" and getmetatable(value) == DirectoryPath
end

--[=[
	@within DirectoryPath
	@tag must_use

	Checks if the directory exists.

	An error will be thrown in the following situations:

	* The current process lacks permissions to read at the directory path.
	* Some other I/O error occurred.

	@param self DirectoryPath -- The DirectoryPath instance
	@return boolean -- Whether the directory exists
]=]
function DirectoryPath.isDir(self)
	return fs.isDir(self.path)
end

--[=[
	@within DirectoryPath
	@tag must_use

	Reads the contents of the directory.

	An error will be thrown in the following situations:

	* The directory does not exist.
	* The current process lacks permissions to read at the directory path.
	* Some other I/O error occurred.

	### Example
	```lua
	local dirPath = DirectoryPath.fromExisting("path/to/dir")
	for _, name in dirPath:readDir() do
		print(name)
	end
	```

	@param self DirectoryPath -- The DirectoryPath instance
	@return { string } -- A table containing the entries in the directory
]=]
function DirectoryPath.readDir(self)
	return fs.readDir(self.path)
end

--[=[
	@within DirectoryPath

	Writes the directory to the filesystem.

	An error will be thrown in the following situations:

	* A file already exists at the directory path.
	* The current process lacks permissions to write the directory.
	* Some other I/O error occurred.

	@param self DirectoryPath -- The DirectoryPath instance
]=]
function DirectoryPath.writeDir(self)
	return fs.writeDir(self.path)
end

--[=[
	@within DirectoryPath

	Removes the directory from the filesystem.

	An error will be thrown in the following situations:

	* The directory does not exist.
	* The current process lacks permissions to remove the directory.
	* Some other I/O error occurred.

	@param self DirectoryPath -- The DirectoryPath instance
]=]
function DirectoryPath.removeDir(self)
	return fs.removeDir(self.path)
end

return DirectoryPath


local luauPath = require("../luau_packages/luau_path")
local Path = luauPath.Path
local fs = require("./fs")
local types = require("./types")

local PathType = types.PathType
local AsPathType = types.AsPathType
local ContentsType = types.ContentsType
local optionalBooleanType = types.optionalBooleanType
local stringType = types.stringType
local booleanType = types.booleanType

export type FilePath = typeof(setmetatable(
	{} :: {
		path: typeof(PathType:type()),
	},
	{} :: FilePathImpl
))

type FilePathImpl = {
	__index: FilePathImpl,
	__tostring: (self: FilePath) -> string,
	new: (filePath: typeof(AsPathType:type())) -> FilePath,
	fromExisting: (filePath: typeof(AsPathType:type())) -> FilePath,
	withFileWritten: (
		self: FilePath,
		contents: typeof(ContentsType:type()),
		allowOverwrite: typeof(optionalBooleanType:type())
	) -> FilePath,
	is: (value: any) -> boolean,
	isFile: (self: FilePath) -> typeof(booleanType:type()),
	readFile: (self: FilePath) -> typeof(stringType:type()),
	writeFile: (self: FilePath, contents: typeof(ContentsType:type())) -> (),
	removeFile: (self: FilePath) -> (),
}

--[=[
	@class FilePath

	Represents a file path.

	### Example
	```lua
	local filePath = FilePath.fromExisting("path/to/file")
	filePath:writeFile("content")
	```
]=]
local FilePath = {} :: FilePathImpl
FilePath.__index = FilePath
--[=[
	@within FilePath
	@prop path Path
	The underlying `Path` instance.
]=]

--[=[
	@within FilePath

	Returns a string representation of the `FilePath` instance.

	### Example
	```lua
	local filePath = FilePath.fromExisting("path/to/file")
	print(filePath) -- Output: FilePath<path/to/file>
	```

	@param self FilePath -- The FilePath instance
	@return string -- A string representation of the FilePath instance
]=]
function FilePath.__tostring(self)
	assert(PathType(self.path))

	return `FilePath<{self.path}>`
end

--[=[
	@within FilePath

	Creates a new `FilePath` instance.

	@param filePath AsPath -- The file path
	@return FilePath -- A new `FilePath` instance
]=]
function FilePath.new(filePath)
	assert(AsPathType(filePath))

	return setmetatable({
		path = Path.from(filePath),
	}, FilePath)
end

--[=[
	@within FilePath

	Creates a new `FilePath` instance from an existing file path.

	An error will be thrown if there is no file at the given path.

	@param filePath AsPath -- The existing file path
	@return FilePath -- A new `FilePath` instance
]=]
function FilePath.fromExisting(filePath)
	assert(AsPathType(filePath))

	if not fs.isFile(filePath) then
		error(`There is no file at that path({filePath})`)
	end

	return setmetatable({
		path = Path.from(filePath),
	}, FilePath)
end

--[=[
	@within FilePath

	Writes the file to the filesystem.

	An error will be thrown in the following situations:

	* The current process lacks permissions to write the file.
	* Some other I/O error occurred.

	### Example
	```lua
	local filePath = FilePath.new("path/to/file"):withFileWritten("content")
	```

	@param self FilePath -- The FilePath instance
	@param contents Contents -- The contents to write to the file
	@param allowOverwrite boolean? -- Whether to allow overwriting an existing file. Defaults to `false`.
	@return FilePath -- The `FilePath` instance
]=]
function FilePath.withFileWritten(self, contents, allowOverwrite)
	assert(ContentsType(contents))
	assert(optionalBooleanType(allowOverwrite))

	if self:isFile() then
		if allowOverwrite then
			self:removeFile()
		else
			error(`Not allowed to overwrite a file. A file already exists at that path({self.path})`)
		end
	end
	if fs.isDir(self.path) then
		error(`Cannot create a new file. The directory already exists at that path({self.path})`)
	end
	self:writeFile(contents)

	return self
end

--[=[
	@within FilePath

	Checks if a value is a `FilePath` instance.

	@param value any -- The value to check
	@return boolean -- Whether the value is a `FilePath` instance
]=]
function FilePath.is(value)
	return type(value) == "table" and getmetatable(value) == FilePath
end

--[=[
	@within FilePath
	@tag must_use

	Checks if the file exists at the path.

	@param self FilePath -- The FilePath instance
	@return boolean -- Whether the file exists at the path
]=]
function FilePath.isFile(self)
	return fs.isFile(self.path)
end

--[=[
	@within FilePath
	@tag must_use

	Reads the file contents.

	An error will be thrown in the following situations:

	* The current process lacks permissions to read the file.
	* Some other I/O error occurred.

	@param self FilePath -- The FilePath instance
	@return string -- The file contents
]=]
function FilePath.readFile(self)
	return fs.readFile(self.path)
end

--[=[
	@within FilePath

	Writes contents to the file.

	An error will be thrown in the following situations:

	* The current process lacks permissions to write the file.
	* Some other I/O error occurred.

	@param self FilePath -- The FilePath instance
	@param contents Contents -- The contents to write to the file
]=]
function FilePath.writeFile(self, contents)
	return fs.writeFile(self.path, contents)
end

--[=[
	@within FilePath

	Removes the file.

	An error will be thrown in the following situations:

	* The file does not exist.
	* The current process lacks permissions to remove the file.
	* Some other I/O error occurred.

	@param self FilePath -- The FilePath instance
]=]
function FilePath.removeFile(self)
	return fs.removeFile(self.path)
end

return FilePath