UNPKG

noshift.js

Version:
1 lines 95.9 kB
{"version":3,"sources":["../src/cli.ts","../src/commands/dev.ts","../src/convert.ts","../src/config.ts","../package.json","../src/header.ts","../src/signal-handler.ts","../src/logger.ts","../src/commands/init.ts","../src/prompt.ts","../src/commands/clean.ts","../src/commands/run.ts","../src/commands/create.ts","../src/commands/compile.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { readFileSync } from \"fs\";\nimport path from \"path\";\nimport dev from \"./commands/dev.js\";\nimport init from \"./commands/init.js\";\nimport clean from \"./commands/clean.js\";\nimport run from \"./commands/run.js\";\nimport create from \"./commands/create.js\";\nimport compile from \"./commands/compile.js\";\n\nfunction loadPackageVersion(): string {\n const pkg = JSON.parse(\n readFileSync(path.join(__dirname, \"../package.json\"), \"utf-8\"),\n ) as { version: string };\n return pkg.version;\n}\n\nconst version = loadPackageVersion();\n\nconst DOCS_URL = \"https://noshift.js.org/\";\n\nconst program = new Command();\n\nprogram\n .name(\"nsc\")\n .description(\"NoShift.js compiler\")\n .version(version, \"-v, --version\", \"output the version number\")\n .option(\"-w, --watch\", \"Watch for file changes and recompile\")\n .option(\"--init\", \"Create a nsjsconfig.json in the current directory\")\n .option(\"--clean\", \"Delete the output directory (outdir)\")\n .option(\"-r, --run <file>\", \"Run a .nsjs file directly\")\n .option(\"--create [name]\", \"Scaffold a new NoShift.js project\")\n .option(\"--no-header\", \"Suppress the generated header comment in output\")\n .addHelpText(\"after\", `\\nDocumentation: ${DOCS_URL}`)\n .action(async (options) => {\n const noHeader = options.header === false;\n if (options.watch) {\n await dev({ noHeader });\n } else if (options.init) {\n await init();\n } else if (options.clean) {\n await clean();\n } else if (options.run) {\n await run(options.run as string, { noHeader });\n } else if (options.create !== undefined) {\n await create((options.create as string) || undefined);\n } else {\n await compile({ noHeader });\n }\n });\n\n// nsc watch\nprogram\n .command(\"watch\")\n .alias(\"w\")\n .description(\"Watch for file changes and recompile\")\n .action(async () => {\n await dev();\n });\n\n// nsc run <file>\nprogram\n .command(\"run <file>\")\n .description(\"Run a .nsjs file directly\")\n .action(async (file: string) => {\n await run(file);\n });\n\n// nsc create [name]\nprogram\n .command(\"create [name]\")\n .description(\"Scaffold a new NoShift.js project\")\n .option(\"--prettier\", \"Include Prettier (default)\")\n .option(\"--no-prettier\", \"Skip Prettier setup\")\n .action(\n async (name: string | undefined, options: Record<string, unknown>) => {\n await create(name, options);\n },\n );\n\n// nsc init\nprogram\n .command(\"init\")\n .description(\"Create a nsjsconfig.json in the current directory\")\n .action(async () => {\n await init();\n });\n\n// nsc clean\nprogram\n .command(\"clean\")\n .description(\"Delete the output directory (outdir)\")\n .action(async () => {\n await clean();\n });\n\n// nsc version\nprogram\n .command(\"version\")\n .description(\"Display the current version\")\n .action(() => {\n console.log(version);\n });\n\n// nsc help\nprogram\n .command(\"help\")\n .description(\"Show help information\")\n .action(() => {\n program.outputHelp();\n });\n\nprogram.parse();\n","import { promises as fs, watch } from \"fs\";\nimport path from \"path\";\nimport convert, { diagnose } from \"../convert.js\";\nimport { loadConfig } from \"../config.js\";\nimport { addHeader } from \"../header.js\";\nimport { handleSigint } from \"../signal-handler.js\";\nimport * as logger from \"../logger.js\";\n\ninterface DevCliOptions {\n noHeader?: boolean;\n}\n\nfunction timestamp(): string {\n return new Date().toLocaleTimeString(\"en-GB\"); // HH:MM:SS\n}\n\nasync function findNsjsFiles(dir: string): Promise<string[] | null> {\n let entries;\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return null;\n }\n const files: string[] = [];\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n const nested = await findNsjsFiles(fullPath);\n if (nested) files.push(...nested);\n } else if (entry.name.endsWith(\".nsjs\") && !entry.name.startsWith(\"_\")) {\n files.push(fullPath);\n }\n }\n return files;\n}\n\nasync function compileFile(\n file: string,\n rootDir: string,\n outDir: string,\n cwd: string,\n convertOptions: { capitalizeInStrings?: boolean } = {},\n noHeader: boolean = false,\n): Promise<void> {\n const relative = path.relative(rootDir, file).replace(/\\\\/g, \"/\");\n const destPath = path\n .join(outDir, path.relative(rootDir, file))\n .replace(/\\.nsjs$/, \".js\");\n const code = await fs.readFile(file, \"utf-8\");\n\n // 構文エラーチェック\n const syntaxErrors = diagnose(code);\n if (syntaxErrors.length > 0) {\n const rel = relative;\n for (const e of syntaxErrors) {\n logger.errorCode(\"NS1\", `${rel}:${e.line}:${e.column} - ${e.message}`);\n }\n throw new Error(`${syntaxErrors.length} syntax error(s)`);\n }\n\n let js = convert(code, convertOptions);\n if (!noHeader) {\n js = addHeader(js);\n }\n await fs.mkdir(path.dirname(destPath), { recursive: true });\n await fs.writeFile(destPath, js, \"utf-8\");\n logger.dim(\n `[${timestamp()}] ${relative} → ${path.relative(cwd, destPath).replace(/\\\\/g, \"/\")}`,\n );\n}\n\nexport default async function dev(\n cliOptions: DevCliOptions = {},\n): Promise<void> {\n const cwd = process.cwd();\n\n let config;\n try {\n config = await loadConfig(cwd);\n } catch (e) {\n logger.errorCode(\"NS0\", (e as Error).message);\n process.exit(1);\n }\n\n const rootDir = path.resolve(cwd, config.compileroptions.rootdir);\n const outDir = path.resolve(cwd, config.compileroptions.outdir);\n const convertOptions = {\n capitalizeInStrings: config.compileroptions.capitalizeinstrings !== false,\n };\n\n // 初回フルコンパイル\n const files = await findNsjsFiles(rootDir);\n if (files === null) {\n logger.errorCode(\n \"NS0\",\n `rootdir '${config.compileroptions.rootdir}' not found.`,\n );\n process.exit(1);\n }\n\n logger.info(`Starting compilation in watch mode...`);\n\n await fs.mkdir(outDir, { recursive: true });\n\n const noHeader = cliOptions.noHeader || config.compileroptions.noheader;\n\n for (const file of files) {\n try {\n await compileFile(file, rootDir, outDir, cwd, convertOptions, noHeader);\n } catch (e) {\n const rel = path.relative(rootDir, file).replace(/\\\\/g, \"/\");\n logger.errorCode(\"NS1\", `${rel}: ${(e as Error).message}`);\n }\n }\n\n logger.info(\n `Watching for file changes in '${logger.highlight(config.compileroptions.rootdir)}'... (Press Ctrl+C to stop)`,\n );\n console.log(\"\");\n\n // Ctrl+C で終了\n handleSigint(() => {\n logger.info(\"Stopped watching.\");\n });\n\n // デバウンス用マップ (ファイルパス → タイマーID)\n const debounceMap = new Map<string, ReturnType<typeof setTimeout>>();\n const DEBOUNCE_MS = 100;\n\n watch(rootDir, { recursive: true }, (_eventType, filename) => {\n if (!filename) return;\n if (!filename.endsWith(\".nsjs\")) return;\n if (path.basename(filename).startsWith(\"_\")) return;\n\n // 重複イベントをデバウンス\n if (debounceMap.has(filename)) {\n clearTimeout(debounceMap.get(filename)!);\n }\n debounceMap.set(\n filename,\n setTimeout(async () => {\n debounceMap.delete(filename);\n const absPath = path.join(rootDir, filename);\n try {\n await compileFile(\n absPath,\n rootDir,\n outDir,\n cwd,\n convertOptions,\n noHeader,\n );\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") {\n // ファイルが削除された場合はスキップ\n } else {\n logger.errorCode(\n \"NS1\",\n `${filename.replace(/\\\\/g, \"/\")}: ${(e as Error).message}`,\n );\n }\n }\n }, DEBOUNCE_MS),\n );\n });\n\n // Ctrl+C で終了するまで待機\n await new Promise(() => {});\n}\n","// ======\n// NoShift.js → JavaScript 置換用マップ\n// ======\nconst noShiftMap: Record<string, string> = {\n // \"^@^@^@\": \"```\", // マルチバックチック\n \"^4^[\": \"${\", // テンプレート式展開開始\n \"^0\": \"^\",\n \"^1\": \"!\",\n \"^2\": '\"',\n \"^3\": \"#\",\n \"^4\": \"$\",\n \"^5\": \"%\",\n \"^7\": \"'\",\n \"^8\": \"(\",\n \"^9\": \")\",\n \"^-\": \"=\",\n \"^^\": \"~\",\n \"^\\\\\": \"_\",\n \"^@\": \"`\",\n \"^[\": \"{\",\n \"^]\": \"}\",\n \"^;\": \"+\",\n \"^:\": \"*\",\n \"^,\": \"<\",\n \"^.\": \">\",\n \"^/\": \"?\",\n};\n\n// 逆引きマップ: JavaScript 記号 → NoShift 表記(単一記号のみ)\nconst symbolToNoshift: Record<string, string> = Object.fromEntries(\n Object.entries(noShiftMap)\n .filter(([key, value]) => key.length === 2 && value.length === 1)\n .map(([key, value]) => [value, key]),\n);\n\ninterface ConvertOptions {\n capitalizeInStrings?: boolean;\n _silent?: boolean;\n}\n\n// ======\n// キーワード置換マップ(コード部分のみ適用、文字列・コメント内はスキップ)\n// 長いものから先にマッチさせる\n// ======\nconst keywordReplacements: [string, string][] = [\n [\"@or^-\", \"|=\"],\n [\"@and^-\", \"&=\"],\n [\"or^-\", \"||=\"],\n [\"and^-\", \"&&=\"],\n [\"@or\", \"|\"],\n [\"@and\", \"&\"],\n];\n\n// 単語境界で囲まれたキーワード(変数名との衝突を防ぐ)\nconst wordKeywordReplacements: [string, string][] = [\n [\"or\", \"||\"],\n [\"and\", \"&&\"],\n];\n\nexport default function convertNsjsToJs(\n nsjsCode: string,\n options: ConvertOptions = {},\n): string {\n const capitalizeInStrings = options.capitalizeInStrings !== false;\n let jsCode = \"\";\n let i = 0;\n const len_ns = nsjsCode.length;\n\n // ======\n // 各種状態 (State) を定義\n // ======\n const STATE = {\n NORMAL: \"NORMAL\", // 普通のコード\n IN_DQ_STRING: \"IN_DQ_STRING\", // \" … \" の中\n IN_SQ_STRING: \"IN_SQ_STRING\", // ' … ' の中\n IN_BT_SINGLE_STRING: \"IN_BT_SINGLE_STRING\", // ` … ` の中\n IN_BT_MULTI_STRING: \"IN_BT_MULTI_STRING\", // ``` … ``` の中\n IN_TEMPLATE_EXPRESSION: \"IN_TEMPLATE_EXPRESSION\", // ${ … } の中\n RAW_DQ_IN_EXPR: \"RAW_DQ_IN_EXPR\", // テンプレート式内の \" … \" の中 (NoShift 変換なし)\n RAW_SQ_IN_EXPR: \"RAW_SQ_IN_EXPR\", // テンプレート式内の ' … ' の中 (NoShift 変換なし)\n IN_LINE_COMMENT: \"IN_LINE_COMMENT\", // // … の中\n IN_BLOCK_COMMENT: \"IN_BLOCK_COMMENT\", // /^: … ^:/ の中\n } as const;\n\n type StateType = (typeof STATE)[keyof typeof STATE];\n\n let currentState: StateType = STATE.NORMAL;\n const stateStack: StateType[] = [];\n\n // マッピングキーは長いものからマッチさせる\n const sortedNsKeys = Object.keys(noShiftMap).sort(\n (a, b) => b.length - a.length,\n );\n\n /**\n * tryConsumeNsjsSequence:\n * - カーソル i の位置に noShiftMap のキーまたは特殊シーケンスがあれば消費し、\n * jsCode に出力して true を返す。なければ false を返す。\n *\n * 優先度:\n * 1) テンプレートリテラル中の式展開開始 (^4^[ / ^4[)\n * 2) テンプレート式中の式閉じ (^])\n * 3) 通常コード/テンプレート式外の文字列開閉 (^2, ^7, ^@^@^@, ^@)\n * 4) テンプレート式中の JS 文字列開閉 (^2, ^7 → RAW_DQ_IN_EXPR / RAW_SQ_IN_EXPR)\n * 5) RAW_DQ_IN_EXPR/RAW_SQ_IN_EXPR 内の終端 (^2 / ^7 → 戻る)\n * 6) 通常の NoShift 置換 (NORMAL または IN_TEMPLATE_EXPRESSION のとき)\n */\n function tryConsumeNsjsSequence(): boolean {\n let allowGeneral = false;\n if (\n currentState === STATE.NORMAL ||\n currentState === STATE.IN_TEMPLATE_EXPRESSION\n ) {\n allowGeneral = true;\n }\n\n for (const nsKey of sortedNsKeys) {\n if (!nsjsCode.startsWith(nsKey, i)) continue;\n\n // ======\n // 1) テンプレートリテラル中の式展開開始\n // ======\n if (\n (nsKey === \"^4^[\" || nsjsCode.startsWith(\"^4[\", i)) &&\n (currentState === STATE.IN_BT_SINGLE_STRING ||\n currentState === STATE.IN_BT_MULTI_STRING)\n ) {\n jsCode += \"${\";\n i += nsKey === \"^4^[\" ? nsKey.length : 3;\n stateStack.push(currentState);\n currentState = STATE.IN_TEMPLATE_EXPRESSION;\n return true;\n }\n\n // ======\n // 2) テンプレート式中の式閉じ\n // ======\n if (nsKey === \"^]\" && currentState === STATE.IN_TEMPLATE_EXPRESSION) {\n jsCode += \"}\";\n i += nsKey.length;\n currentState = stateStack.pop()!;\n return true;\n }\n\n // ======\n // 3) 通常コード/テンプレート式外の文字列開閉\n // ======\n\n // 3-1) \"^2\": ダブルクォート開閉\n if (nsKey === \"^2\" && currentState === STATE.NORMAL) {\n jsCode += '\"';\n i += nsKey.length;\n stateStack.push(currentState);\n currentState = STATE.IN_DQ_STRING;\n return true;\n }\n if (nsKey === \"^2\" && currentState === STATE.IN_DQ_STRING) {\n jsCode += '\"';\n i += nsKey.length;\n currentState = stateStack.pop()!;\n return true;\n }\n\n // 3-2) \"^7\": シングルクォート開閉\n if (nsKey === \"^7\" && currentState === STATE.NORMAL) {\n jsCode += \"'\";\n i += nsKey.length;\n stateStack.push(currentState);\n currentState = STATE.IN_SQ_STRING;\n return true;\n }\n if (nsKey === \"^7\" && currentState === STATE.IN_SQ_STRING) {\n jsCode += \"'\";\n i += nsKey.length;\n currentState = stateStack.pop()!;\n return true;\n }\n\n // このブロックを削除\n // 3-3) \"^@^@^@\": マルチバックチック開閉\n /*\n if (\n nsKey === \"^@^@^@\" &&\n (currentState === STATE.NORMAL ||\n currentState === STATE.IN_TEMPLATE_EXPRESSION)\n ) {\n jsCode += \"```\";\n i += nsKey.length;\n stateStack.push(currentState);\n currentState = STATE.IN_BT_MULTI_STRING;\n return true;\n }\n if (nsKey === \"^@^@^@\" && currentState === STATE.IN_BT_MULTI_STRING) {\n jsCode += \"```\";\n i += nsKey.length;\n currentState = stateStack.pop()!;\n return true;\n }\n */\n\n // 3-4) \"^@\": シングルバックチック開閉\n if (\n nsKey === \"^@\" &&\n (currentState === STATE.NORMAL ||\n currentState === STATE.IN_TEMPLATE_EXPRESSION)\n ) {\n jsCode += \"`\";\n i += nsKey.length;\n stateStack.push(currentState);\n currentState = STATE.IN_BT_SINGLE_STRING;\n return true;\n }\n if (nsKey === \"^@\" && currentState === STATE.IN_BT_SINGLE_STRING) {\n jsCode += \"`\";\n i += nsKey.length;\n currentState = stateStack.pop()!;\n return true;\n }\n\n // ======\n // 4) テンプレート式中の JS 文字列開閉\n // ======\n if (nsKey === \"^2\" && currentState === STATE.IN_TEMPLATE_EXPRESSION) {\n jsCode += '\"';\n i += nsKey.length;\n stateStack.push(currentState);\n currentState = STATE.RAW_DQ_IN_EXPR;\n return true;\n }\n if (nsKey === \"^7\" && currentState === STATE.IN_TEMPLATE_EXPRESSION) {\n jsCode += \"'\";\n i += nsKey.length;\n stateStack.push(currentState);\n currentState = STATE.RAW_SQ_IN_EXPR;\n return true;\n }\n\n // ======\n // 5) RAW_DQ_IN_EXPR / RAW_SQ_IN_EXPR 内の終端\n // ======\n if (nsKey === \"^2\" && currentState === STATE.RAW_DQ_IN_EXPR) {\n jsCode += '\"';\n i += nsKey.length;\n currentState = stateStack.pop()!;\n return true;\n }\n if (nsKey === \"^7\" && currentState === STATE.RAW_SQ_IN_EXPR) {\n jsCode += \"'\";\n i += nsKey.length;\n currentState = stateStack.pop()!;\n return true;\n }\n\n // ======\n // 6) 通常の NoShift 置換 (NORMAL or IN_TEMPLATE_EXPRESSION のとき)\n // ======\n if (allowGeneral) {\n jsCode += noShiftMap[nsKey];\n i += nsKey.length;\n return true;\n }\n }\n\n return false;\n }\n\n // ======\n // メインループ\n // ======\n while (i < len_ns) {\n let consumed = false;\n\n // ======\n // ステップ1: 文字列内でのエスケープ処理\n // ======\n\n // (A) IN_DQ_STRING 内 (\" … \")\n if (currentState === STATE.IN_DQ_STRING) {\n if (nsjsCode.startsWith(\"\\\\^6\", i)) {\n jsCode += \"^6\"; // \"\\^6\" を文字列中のリテラル \"^6\" として出力\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\^2\", i)) {\n jsCode += \"^2\"; // \"\\^2\" を文字列中の \"^2\" として出力\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\\\\\\", i)) {\n jsCode += \"\\\\\\\\\\\\\\\\\"; // \"\\\\\" を JSコード内で \"\\\\\\\\\" に出力\n i += 2;\n consumed = true;\n }\n }\n // (B) IN_SQ_STRING 内 (' … ')\n else if (currentState === STATE.IN_SQ_STRING) {\n if (nsjsCode.startsWith(\"\\\\^6\", i)) {\n jsCode += \"^6\";\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\^7\", i)) {\n jsCode += \"^7\";\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\\\\\\", i)) {\n jsCode += \"\\\\\\\\\\\\\\\\\";\n i += 2;\n consumed = true;\n }\n }\n // (C) RAW_DQ_IN_EXPR 内 (テンプレート式中の \" … \")\n else if (currentState === STATE.RAW_DQ_IN_EXPR) {\n if (nsjsCode.startsWith(\"\\\\^6\", i)) {\n jsCode += \"^6\";\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\^2\", i)) {\n jsCode += \"^2\";\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\\\\\\", i)) {\n jsCode += \"\\\\\\\\\\\\\\\\\";\n i += 2;\n consumed = true;\n } else if (nsjsCode.startsWith(\"^2\", i)) {\n jsCode += '\"'; // 終端\n i += 2;\n currentState = stateStack.pop()!;\n consumed = true;\n }\n if (consumed) {\n continue; // 生文字扱いなので NoShift 変換せず次へ\n } else {\n jsCode += nsjsCode[i];\n i += 1;\n continue;\n }\n }\n // (D) RAW_SQ_IN_EXPR 内 (テンプレート式中の ' … ')\n else if (currentState === STATE.RAW_SQ_IN_EXPR) {\n if (nsjsCode.startsWith(\"\\\\^6\", i)) {\n jsCode += \"^6\";\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\^7\", i)) {\n jsCode += \"^7\";\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\\\\\\", i)) {\n jsCode += \"\\\\\\\\\\\\\\\\\";\n i += 2;\n consumed = true;\n } else if (nsjsCode.startsWith(\"^7\", i)) {\n jsCode += \"'\";\n i += 2;\n currentState = stateStack.pop()!;\n consumed = true;\n }\n if (consumed) {\n continue;\n } else {\n jsCode += nsjsCode[i];\n i += 1;\n continue;\n }\n }\n // (E) IN_BT_SINGLE_STRING 内 (` … `)\n else if (currentState === STATE.IN_BT_SINGLE_STRING) {\n if (nsjsCode.startsWith(\"\\\\^6\", i)) {\n jsCode += \"^6\";\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\^@\", i)) {\n jsCode += \"^@\";\n i += 3;\n consumed = true;\n } else if (nsjsCode.startsWith(\"\\\\\\\\\", i)) {\n jsCode += \"\\\\\\\\\\\\\\\\\";\n i += 2;\n consumed = true;\n }\n }\n // (F) IN_BT_MULTI_STRING 内 (``` … ```)\n else if (currentState === STATE.IN_BT_MULTI_STRING) {\n /* if (nsjsCode.startsWith(\"\\\\^@^@^@\", i)) {\n jsCode += \"^@^@^@\";\n i += 7;\n consumed = true;\n } else */\n if (nsjsCode.startsWith(\"\\\\\\\\\", i)) {\n jsCode += \"\\\\\\\\\\\\\\\\\";\n i += 2;\n consumed = true;\n }\n }\n\n // ======\n // ステップ2: ^6 大文字化モディファイア (RAW 状態とコメント以外で動作)\n // - コード上 (NORMAL, IN_TEMPLATE_EXPRESSION) では常に有効\n // - 文字列内は capitalizeInStrings オプションに従う\n // - RAW_DQ_IN_EXPR / RAW_SQ_IN_EXPR は else-if の continue で既に除外済み\n // ======\n if (\n !consumed &&\n currentState !== STATE.IN_LINE_COMMENT &&\n currentState !== STATE.IN_BLOCK_COMMENT\n ) {\n if (nsjsCode.startsWith(\"^6\", i)) {\n const inString =\n currentState === STATE.IN_DQ_STRING ||\n currentState === STATE.IN_SQ_STRING ||\n currentState === STATE.IN_BT_SINGLE_STRING ||\n currentState === STATE.IN_BT_MULTI_STRING;\n if (!inString || capitalizeInStrings) {\n i += 2;\n if (i < len_ns) {\n jsCode += nsjsCode[i].toUpperCase();\n i += 1;\n }\n consumed = true;\n }\n }\n }\n\n // ======\n // ステップ2.5: コメントの処理\n // ======\n if (!consumed) {\n // 行コメント開始 (//)\n if (currentState === STATE.NORMAL && nsjsCode.startsWith(\"//\", i)) {\n jsCode += \"//\";\n i += 2;\n stateStack.push(currentState);\n currentState = STATE.IN_LINE_COMMENT;\n consumed = true;\n }\n // 行コメント終了 (改行)\n else if (currentState === STATE.IN_LINE_COMMENT) {\n if (nsjsCode[i] === \"\\n\") {\n jsCode += \"\\n\";\n i += 1;\n currentState = stateStack.pop()!;\n } else {\n jsCode += nsjsCode[i];\n i += 1;\n }\n consumed = true;\n }\n // ブロックコメント開始 (/^:)\n else if (currentState === STATE.NORMAL && nsjsCode.startsWith(\"/^:\", i)) {\n jsCode += \"/*\";\n i += 3;\n stateStack.push(currentState);\n currentState = STATE.IN_BLOCK_COMMENT;\n consumed = true;\n }\n // ブロックコメント終了 (^:/)\n else if (\n currentState === STATE.IN_BLOCK_COMMENT &&\n nsjsCode.startsWith(\"^:/\", i)\n ) {\n jsCode += \"*/\";\n i += 3;\n currentState = stateStack.pop()!;\n consumed = true;\n }\n // ブロックコメント内の文字 (そのまま出力)\n else if (currentState === STATE.IN_BLOCK_COMMENT) {\n jsCode += nsjsCode[i];\n i += 1;\n consumed = true;\n }\n }\n\n // ======\n // ステップ2.7: キーワード置換 (NORMAL / IN_TEMPLATE_EXPRESSION のみ)\n // ======\n if (\n !consumed &&\n (currentState === STATE.NORMAL ||\n currentState === STATE.IN_TEMPLATE_EXPRESSION)\n ) {\n // @or^- / @and^- / or^- / and^- / @or / @and\n for (const [kw, replacement] of keywordReplacements) {\n if (nsjsCode.startsWith(kw, i)) {\n jsCode += replacement;\n i += kw.length;\n consumed = true;\n break;\n }\n }\n // 単語境界キーワード: or / and\n if (!consumed) {\n for (const [kw, replacement] of wordKeywordReplacements) {\n if (nsjsCode.startsWith(kw, i)) {\n // 前方の境界チェック\n const prevChar = i > 0 ? nsjsCode[i - 1] : \" \";\n const prevIsBoundary = !/[a-zA-Z0-9_$]/.test(prevChar);\n // 後方の境界チェック\n const afterPos = i + kw.length;\n const nextChar = afterPos < len_ns ? nsjsCode[afterPos] : \" \";\n const nextIsBoundary = !/[a-zA-Z0-9_$]/.test(nextChar);\n if (prevIsBoundary && nextIsBoundary) {\n jsCode += replacement;\n i += kw.length;\n consumed = true;\n break;\n }\n }\n }\n }\n }\n\n // ======\n // ステップ3: NoShift シーケンスや文字列/テンプレートの開閉、式展開を試す\n // ======\n if (!consumed) {\n consumed = tryConsumeNsjsSequence();\n }\n\n // ======\n // ステップ4: 何も消費しなかったら文字をそのまま出力\n // ======\n if (!consumed) {\n jsCode += nsjsCode[i];\n i += 1;\n }\n }\n\n // スタックに残りがあればリテラル/テンプレートの閉じ忘れ\n if (stateStack.length > 0) {\n // diagnostics モードでない場合のみ警告を出す\n if (!options._silent) {\n console.warn(\n `Warning: Unmatched literal/templating states. Final state: ${currentState}, Remaining stack: ${stateStack.join(\n \", \",\n )}`,\n );\n }\n }\n return jsCode;\n}\n\nexport interface UppercaseWarning {\n line: number;\n column: number;\n char: string;\n message: string;\n}\n\n/**\n * ソースコード内の大文字、_, $, # の使用を警告する。\n * コメント内は無視する。\n * capitalizeInStrings が true の場合、文字列内の大文字も警告する。\n */\nexport function checkUppercaseWarnings(\n nsjsCode: string,\n options: { capitalizeInStrings?: boolean } = {},\n): UppercaseWarning[] {\n const capitalizeInStrings = options.capitalizeInStrings !== false;\n const warnings: UppercaseWarning[] = [];\n const lines = nsjsCode.split(\"\\n\");\n\n // シンプルな状態追跡(文字列・コメント内を除外)\n let inDQ = false; // ^2...^2\n let inSQ = false; // ^7...^7\n let inBT = false; // ^@...^@\n let inLineComment = false;\n let inBlockComment = false;\n\n // 先頭行の shebang 検出\n const firstLine = lines[0] ?? \"\";\n const hasNoShiftShebang = firstLine.startsWith(\"#^1\"); // #^1 → valid NoShift shebang\n const hasRawShebang = firstLine.startsWith(\"#!\"); // #! → raw shebang(警告対象)\n\n // #! を使っている場合は警告\n if (hasRawShebang) {\n warnings.push({\n line: 1,\n column: 1,\n char: \"#\",\n message: \"Shebang '#!' found. Use '#^1' instead.\",\n });\n }\n\n for (let lineNum = 0; lineNum < lines.length; lineNum++) {\n const line = lines[lineNum];\n inLineComment = false; // 行コメントは行ごとにリセット\n\n // 先頭行の shebang 行はスキップ(#^1 / #! どちらも行全体をスキップ)\n if (lineNum === 0 && (hasNoShiftShebang || hasRawShebang)) {\n continue;\n }\n\n for (let col = 0; col < line.length; col++) {\n const ch = line[col];\n const next = line[col + 1];\n\n // エスケープ (\\^2, \\^7, \\^@) をスキップ\n if (ch === \"\\\\\" && next === \"^\") {\n col += 2;\n continue;\n }\n\n // ブロックコメント終了 (^:/)\n if (\n inBlockComment &&\n ch === \"^\" &&\n next === \":\" &&\n line[col + 2] === \"/\"\n ) {\n inBlockComment = false;\n col += 2;\n continue;\n }\n if (inBlockComment) continue;\n\n // 行コメント開始\n if (!inDQ && !inSQ && !inBT && ch === \"/\" && next === \"/\") {\n inLineComment = true;\n break;\n }\n // ブロックコメント開始 (/^:)\n if (\n !inDQ &&\n !inSQ &&\n !inBT &&\n ch === \"/\" &&\n next === \"^\" &&\n line[col + 2] === \":\"\n ) {\n inBlockComment = true;\n col += 2;\n continue;\n }\n\n if (inLineComment) continue;\n\n // ^6 モディファイア → 次の文字はスキップ(意図的な大文字化)\n if (ch === \"^\" && next === \"6\") {\n col += 2; // ^6 と次の文字をスキップ\n continue;\n }\n\n // 文字列リテラルの開閉\n if (ch === \"^\" && next === \"2\") {\n inDQ = !inDQ;\n col += 1;\n continue;\n }\n if (ch === \"^\" && next === \"7\") {\n inSQ = !inSQ;\n col += 1;\n continue;\n }\n if (ch === \"^\" && next === \"@\") {\n inBT = !inBT;\n col += 1;\n continue;\n }\n\n // 文字列内の処理\n if (inDQ || inSQ || inBT) {\n // capitalizeInStrings が有効なら、文字列内の大文字も警告\n if (capitalizeInStrings && /[A-Z]/.test(ch)) {\n warnings.push({\n line: lineNum + 1,\n column: col + 1,\n char: ch,\n message: `Uppercase letter '${ch}' found in string. Use ^6${ch.toLowerCase()} instead.`,\n });\n }\n continue;\n }\n\n // 他の ^X シーケンスをスキップ\n if (ch === \"^\" && next && /[0-9\\-^\\\\@\\[\\];:,./]/.test(next)) {\n col += 1;\n continue;\n }\n\n // 大文字の警告\n if (/[A-Z]/.test(ch)) {\n warnings.push({\n line: lineNum + 1,\n column: col + 1,\n char: ch,\n message: `Uppercase letter '${ch}' found. Use ^6${ch.toLowerCase()} instead.`,\n });\n }\n // Shift キーが必要な記号の警告(_ と # は別途警告)\n else if (symbolToNoshift[ch] && ch !== \"_\" && ch !== \"#\") {\n warnings.push({\n line: lineNum + 1,\n column: col + 1,\n char: ch,\n message: `Symbol '${ch}' found. Use ${symbolToNoshift[ch]} instead.`,\n });\n }\n // _ の警告(^\\ で代用可能)\n else if (ch === \"_\") {\n warnings.push({\n line: lineNum + 1,\n column: col + 1,\n char: ch,\n message: \"Underscore '_' found in code. Use ^\\\\ instead.\",\n });\n }\n // # の警告(^3 で入力可能)\n else if (ch === \"#\") {\n warnings.push({\n line: lineNum + 1,\n column: col + 1,\n char: ch,\n message: \"Hash '#' found in code. Use ^3 instead.\",\n });\n }\n }\n }\n\n return warnings;\n}\n\n// ======\n// 有効な ^X シーケンスの一覧(^3 は別扱い)\n// ======\nconst validCaretKeys = new Set(Object.keys(noShiftMap).map((k) => k[1]));\n// ^6 (capitalize) も有効\nvalidCaretKeys.add(\"6\");\n\nexport interface DiagnosticError {\n line: number;\n column: number;\n message: string;\n}\n\n/**\n * NoShift.js ソースコードの構文エラーを検出する。\n *\n * 検出するエラー:\n * - 閉じられていない文字列リテラル (^2, ^7, ^@)\n * - 閉じられていないブロックコメント (/^:)\n * - 閉じられていないテンプレート式 (^4^[)\n * - ファイル末尾の ^6 (大文字化対象の文字がない)\n * - 不明な ^ シーケンス\n */\nexport function diagnose(nsjsCode: string): DiagnosticError[] {\n const errors: DiagnosticError[] = [];\n const lines = nsjsCode.split(\"\\n\");\n\n // 状態: \"NORMAL\" | \"DQ\" | \"SQ\" | \"BT\" | \"BLOCK_COMMENT\" | \"LINE_COMMENT\" | \"TEMPLATE_EXPR\"\n type DiagnoseState =\n | \"NORMAL\"\n | \"DQ\"\n | \"SQ\"\n | \"BT\"\n | \"BLOCK_COMMENT\"\n | \"LINE_COMMENT\"\n | \"TEMPLATE_EXPR\";\n\n let state: DiagnoseState = \"NORMAL\";\n const stateStack: DiagnoseState[] = [];\n // 開始位置のスタック (エラー報告用)\n interface OpenPosition {\n line: number;\n column: number;\n type: string;\n }\n const openPositions: OpenPosition[] = [];\n\n for (let lineNum = 0; lineNum < lines.length; lineNum++) {\n const line = lines[lineNum];\n\n // 行コメントは行ごとにリセット\n if (state === \"LINE_COMMENT\") {\n state = stateStack.pop() || \"NORMAL\";\n }\n\n for (let col = 0; col < line.length; col++) {\n const ch = line[col];\n const next = col + 1 < line.length ? line[col + 1] : undefined;\n const next2 = col + 2 < line.length ? line[col + 2] : undefined;\n\n // エスケープ (\\^X) をスキップ\n if (ch === \"\\\\\" && next === \"^\") {\n col += 2;\n continue;\n }\n // \\\\\\\\ をスキップ\n if (ch === \"\\\\\" && next === \"\\\\\") {\n col += 1;\n continue;\n }\n\n // ── ブロックコメント内 ──\n if (state === \"BLOCK_COMMENT\") {\n if (ch === \"^\" && next === \":\" && next2 === \"/\") {\n state = stateStack.pop() || \"NORMAL\";\n openPositions.pop();\n col += 2;\n }\n continue;\n }\n\n // ── 行コメント内 ──\n if (state === \"LINE_COMMENT\") {\n continue; // 改行で自動リセット(上で処理済み)\n }\n\n // ── 文字列内 ──\n if (state === \"DQ\") {\n if (ch === \"^\" && next === \"2\") {\n state = stateStack.pop() || \"NORMAL\";\n openPositions.pop();\n col += 1;\n }\n continue;\n }\n if (state === \"SQ\") {\n if (ch === \"^\" && next === \"7\") {\n state = stateStack.pop() || \"NORMAL\";\n openPositions.pop();\n col += 1;\n }\n continue;\n }\n if (state === \"BT\") {\n // テンプレート式展開開始\n if (ch === \"^\" && next === \"4\" && (next2 === \"^\" || next2 === \"[\")) {\n if (next2 === \"^\" && col + 3 < line.length && line[col + 3] === \"[\") {\n stateStack.push(state);\n openPositions.push({\n line: lineNum + 1,\n column: col + 1,\n type: \"TEMPLATE_EXPR\",\n });\n state = \"TEMPLATE_EXPR\";\n col += 3;\n } else if (next2 === \"[\") {\n stateStack.push(state);\n openPositions.push({\n line: lineNum + 1,\n column: col + 1,\n type: \"TEMPLATE_EXPR\",\n });\n state = \"TEMPLATE_EXPR\";\n col += 2;\n }\n continue;\n }\n if (ch === \"^\" && next === \"@\") {\n state = stateStack.pop() || \"NORMAL\";\n openPositions.pop();\n col += 1;\n }\n continue;\n }\n\n // ── テンプレート式内 ──\n if (state === \"TEMPLATE_EXPR\") {\n if (ch === \"^\" && next === \"]\") {\n state = stateStack.pop() || \"NORMAL\";\n openPositions.pop();\n col += 1;\n continue;\n }\n // テンプレート式内の文字列もチェック → fall through to NORMAL logic\n }\n\n // ── NORMAL / TEMPLATE_EXPR 共通 ──\n\n // 行コメント開始\n if (ch === \"/\" && next === \"/\") {\n stateStack.push(state);\n state = \"LINE_COMMENT\";\n break; // 行末まで読み飛ばし\n }\n\n // ブロックコメント開始 (/^:)\n if (ch === \"/\" && next === \"^\" && next2 === \":\") {\n stateStack.push(state);\n openPositions.push({\n line: lineNum + 1,\n column: col + 1,\n type: \"BLOCK_COMMENT\",\n });\n state = \"BLOCK_COMMENT\";\n col += 2;\n continue;\n }\n\n // ダブルクォート文字列開始 (^2)\n if (ch === \"^\" && next === \"2\") {\n stateStack.push(state);\n openPositions.push({ line: lineNum + 1, column: col + 1, type: \"DQ\" });\n state = \"DQ\";\n col += 1;\n continue;\n }\n\n // シングルクォート文字列開始 (^7)\n if (ch === \"^\" && next === \"7\") {\n stateStack.push(state);\n openPositions.push({ line: lineNum + 1, column: col + 1, type: \"SQ\" });\n state = \"SQ\";\n col += 1;\n continue;\n }\n\n // テンプレートリテラル開始 (^@)\n if (ch === \"^\" && next === \"@\") {\n stateStack.push(state);\n openPositions.push({ line: lineNum + 1, column: col + 1, type: \"BT\" });\n state = \"BT\";\n col += 1;\n continue;\n }\n\n // ^6 大文字化: 次の文字がなければエラー\n if (ch === \"^\" && next === \"6\") {\n if (col + 2 >= line.length && lineNum === lines.length - 1) {\n errors.push({\n line: lineNum + 1,\n column: col + 1,\n message:\n \"^6 at end of file with no following character to capitalize.\",\n });\n }\n col += 2; // ^6 + 次の1文字をスキップ\n continue;\n }\n\n // その他の ^X シーケンス: 有効性チェック\n if (ch === \"^\" && next !== undefined) {\n if (!validCaretKeys.has(next)) {\n errors.push({\n line: lineNum + 1,\n column: col + 1,\n message: `Unknown sequence '^${next}'.`,\n });\n }\n col += 1;\n continue;\n }\n\n // ファイル末尾の孤立 ^\n if (ch === \"^\" && next === undefined && lineNum === lines.length - 1) {\n errors.push({\n line: lineNum + 1,\n column: col + 1,\n message: \"Lone '^' at end of file.\",\n });\n }\n }\n }\n\n // ── 閉じられていない構造を報告 ──\n while (openPositions.length > 0) {\n const pos = openPositions.pop()!;\n const labels: Record<string, string> = {\n DQ: \"string literal (^2...^2)\",\n SQ: \"string literal (^7...^7)\",\n BT: \"template literal (^@...^@)\",\n BLOCK_COMMENT: \"block comment (/^:...^:/)\",\n TEMPLATE_EXPR: \"template expression (^4^[...^])\",\n };\n errors.push({\n line: pos.line,\n column: pos.column,\n message: `Unclosed ${labels[pos.type] || pos.type} opened here.`,\n });\n }\n\n return errors;\n}\n","import { promises as fs } from \"fs\";\nimport path from \"path\";\n\nexport interface CompilerOptions {\n rootdir: string;\n outdir: string;\n warnuppercase: boolean;\n capitalizeinstrings: boolean;\n noheader: boolean;\n}\n\nexport interface NsjsConfig {\n compileroptions: CompilerOptions;\n}\n\nconst DEFAULT_CONFIG: NsjsConfig = {\n compileroptions: {\n rootdir: \"src\",\n outdir: \"dist\",\n warnuppercase: true,\n capitalizeinstrings: true,\n noheader: false,\n },\n};\n\n/**\n * プロジェクトルートの nsjsconfig.json を読み込む。\n * ファイルが存在しない場合はデフォルト設定を返す。\n */\nexport async function loadConfig(\n cwd: string = process.cwd(),\n): Promise<NsjsConfig> {\n const configPath = path.join(cwd, \"nsjsconfig.json\");\n\n try {\n const raw = await fs.readFile(configPath, \"utf-8\");\n const userConfig = JSON.parse(raw) as Partial<NsjsConfig>;\n\n return {\n compileroptions: {\n ...DEFAULT_CONFIG.compileroptions,\n ...(userConfig.compileroptions ?? {}),\n },\n };\n } catch (e: unknown) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") {\n return DEFAULT_CONFIG;\n }\n throw new Error(`Failed to parse nsjsconfig.json: ${(e as Error).message}`);\n }\n}\n","{\n \"name\": \"noshift.js\",\n \"version\": \"0.15.1\",\n \"description\": \"Joke language.\",\n \"bin\": {\n \"nsc\": \"./dist/cli.cjs\"\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": {\n \"types\": \"./dist/index.d.mts\",\n \"default\": \"./dist/index.mjs\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.cts\",\n \"default\": \"./dist/index.cjs\"\n }\n }\n },\n \"type\": \"module\",\n \"scripts\": {\n \"build\": \"tsup\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"typecheck\": \"tsc --noEmit\",\n \"format\": \"prettier --write ./src/\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/otoneko1102/NoShift.js.git\"\n },\n \"keywords\": [\n \"noshift\",\n \"nsjs\",\n \"joke\",\n \"language\",\n \"lang\"\n ],\n \"author\": \"otoneko.\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/otoneko1102/NoShift.js/issues\"\n },\n \"homepage\": \"https://noshift.js.org\",\n \"dependencies\": {\n \"commander\": \"^14.0.3\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^25.3.0\",\n \"prettier\": \"^3.8.1\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^4.0.18\"\n },\n \"engines\": {\n \"node\": \">=22.12.0\"\n }\n}\n","import pkg from \"../package.json\";\n\nfunction getVersion(): string {\n return pkg.version;\n}\n/**\n * コンパイル済み JS にヘッダーコメントを挿入する。\n * shebang (#!) がある場合はその行の直後に挿入。\n *\n * @param js - コンパイル済み JavaScript\n * @param version - バージョン文字列(省略時は package.json から取得)\n * @returns ヘッダー付き JavaScript\n */\nexport function addHeader(js: string, version?: string): string {\n const ver = version ?? getVersion();\n const header = `// Generated by NoShift.js ${ver}`;\n\n if (js.startsWith(\"#!\")) {\n const newlineIndex = js.indexOf(\"\\n\");\n if (newlineIndex === -1) {\n // shebang のみでコードがない\n return js + \"\\n\" + header + \"\\n\";\n }\n const shebang = js.slice(0, newlineIndex + 1);\n const rest = js.slice(newlineIndex + 1);\n return shebang + header + \"\\n\" + rest;\n }\n\n return header + \"\\n\" + js;\n}\n","// シグナルハンドリングユーティリティ\n\nlet isHandlerRegistered = false;\nlet cleanupCallbacks: (() => void | Promise<void>)[] = [];\n\n/**\n * Ctrl+C (SIGINT) を適切にハンドリングする\n * @param cleanup - クリーンアップ時に実行する関数(オプション)\n */\nexport function handleSigint(cleanup?: () => void | Promise<void>): void {\n if (cleanup) {\n cleanupCallbacks.push(cleanup);\n }\n\n if (!isHandlerRegistered) {\n isHandlerRegistered = true;\n\n process.on(\"SIGINT\", async () => {\n console.log(\"\\n\"); // 改行を追加してきれいに終了\n\n // すべてのクリーンアップコールバックを実行\n for (const cb of cleanupCallbacks) {\n try {\n await cb();\n } catch {\n // エラーは無視\n }\n }\n\n process.exit(0);\n });\n }\n}\n\n/**\n * inquirer のキャンセルエラーをチェック\n */\nexport function isUserCancelled(error: unknown): boolean {\n return (\n error != null &&\n typeof error === \"object\" &&\n ((\"name\" in error && (error as Error).name === \"ExitPromptError\") ||\n (\"message\" in error &&\n (error as Error).message === \"User force closed the prompt\"))\n );\n}\n","// シンプルなロガーユーティリティ(ANSI エスケープコードを使用)\n\nconst colors = {\n reset: \"\\x1b[0m\",\n bright: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n blue: \"\\x1b[34m\",\n magenta: \"\\x1b[35m\",\n cyan: \"\\x1b[36m\",\n gray: \"\\x1b[90m\",\n};\n\nexport function success(message: string): void {\n console.log(`${colors.green}✓${colors.reset} ${message}`);\n}\n\nexport function error(message: string): void {\n console.error(`${colors.red}✗${colors.reset} ${message}`);\n}\n\nexport function info(message: string): void {\n console.log(`${colors.blue}ℹ${colors.reset} ${message}`);\n}\n\nexport function warn(message: string): void {\n console.log(`${colors.yellow}⚠${colors.reset} ${message}`);\n}\n\nexport function step(message: string): void {\n console.log(`${colors.cyan}→${colors.reset} ${message}`);\n}\n\nexport function dim(message: string): void {\n console.log(`${colors.dim}${message}${colors.reset}`);\n}\n\nexport function bold(text: string): string {\n return `${colors.bright}${text}${colors.reset}`;\n}\n\nexport function highlight(text: string): string {\n return `${colors.cyan}${text}${colors.reset}`;\n}\n\nexport function errorCode(code: string, message: string): void {\n console.error(`${colors.red}error ${code}:${colors.reset} ${message}`);\n}\n","import { writeFile, readFile, access } from \"fs/promises\";\nimport path from \"path\";\nimport { handleSigint } from \"../signal-handler.js\";\nimport * as logger from \"../logger.js\";\nimport { askConfirm } from \"../prompt.js\";\n\nconst DEFAULT_CONFIG = {\n compileroptions: {\n rootdir: \"src\",\n outdir: \"dist\",\n warnuppercase: true,\n capitalizeinstrings: true,\n noheader: false,\n },\n};\n\n/** .prettierrc 系のファイル名(優先順) */\nconst PRETTIERRC_FILES = [\n \".prettierrc\",\n \".prettierrc.json\",\n \".prettierrc.yml\",\n \".prettierrc.yaml\",\n \".prettierrc.json5\",\n \".prettierrc.cjs\",\n \".prettierrc.mjs\",\n \"prettier.config.js\",\n \"prettier.config.cjs\",\n \"prettier.config.mjs\",\n];\n\nconst PLUGIN_NAME = \"prettier-plugin-noshift.js\";\n\n/**\n * 既存の .prettierrc / .prettierrc.json を読み込み plugins に追加する。\n * JSON 形式のみ自動編集可能。それ以外は手動追加を案内する。\n */\nasync function addPluginToExistingConfig(filePath: string): Promise<void> {\n const basename = path.basename(filePath);\n const isJson = basename === \".prettierrc\" || basename === \".prettierrc.json\";\n\n if (!isJson) {\n logger.warn(\n `Found ${basename} — please add \"${PLUGIN_NAME}\" to the plugins array manually.`,\n );\n return;\n }\n\n try {\n const raw = await readFile(filePath, \"utf-8\");\n const config = JSON.parse(raw) as Record<string, unknown>;\n const plugins = Array.isArray(config.plugins)\n ? (config.plugins as string[])\n : [];\n\n if (plugins.includes(PLUGIN_NAME)) {\n logger.info(`${basename} already contains \"${PLUGIN_NAME}\".`);\n return;\n }\n\n plugins.push(PLUGIN_NAME);\n config.plugins = plugins;\n await writeFile(filePath, JSON.stringify(config, null, 2) + \"\\n\");\n logger.success(`Added \"${PLUGIN_NAME}\" to ${basename}`);\n } catch (err) {\n logger.error(`Failed to update ${basename}: ${(err as Error).message}`);\n logger.warn(`Please add \"${PLUGIN_NAME}\" to the plugins array manually.`);\n }\n}\n\n/**\n * 新しい .prettierrc を作成する\n */\nasync function createPrettierConfig(): Promise<void> {\n const prettierConfig = {\n semi: true,\n singleQuote: false,\n trailingComma: \"es5\",\n plugins: [PLUGIN_NAME],\n };\n await writeFile(\n \".prettierrc\",\n JSON.stringify(prettierConfig, null, 2) + \"\\n\",\n );\n logger.success(\"Created .prettierrc\");\n}\n\nexport default async function init(): Promise<void> {\n handleSigint();\n\n const cwd = process.cwd();\n const configPath = path.join(cwd, \"nsjsconfig.json\");\n\n // ── nsjsconfig.json ──\n let configExists = false;\n try {\n await access(configPath);\n configExists = true;\n } catch {\n // not found — OK\n }\n\n if (configExists) {\n logger.warn(\"nsjsconfig.json already exists in the current directory.\");\n const overwrite = await askConfirm(\"Overwrite?\", false);\n if (!overwrite) {\n logger.info(\"Skipped nsjsconfig.json\");\n } else {\n await writeFile(\n configPath,\n JSON.stringify(DEFAULT_CONFIG, null, 2) + \"\\n\",\n );\n logger.success(\"Overwritten nsjsconfig.json\");\n }\n } else {\n await writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + \"\\n\");\n logger.success(\"Created nsjsconfig.json\");\n }\n\n logger.dim(\n ` compileroptions.rootdir : ${DEFAULT_CONFIG.compileroptions.rootdir}`,\n );\n logger.dim(\n ` compileroptions.outdir : ${DEFAULT_CONFIG.compileroptions.outdir}`,\n );\n\n // ── Prettier ──\n const usePrettier = await askConfirm(\"Set up Prettier?\", true);\n\n if (usePrettier) {\n // 既存の prettierrc 系ファイルを探す\n let existingFile: string | null = null;\n for (const name of PRETTIERRC_FILES) {\n try {\n await access(path.join(cwd, name));\n existingFile = name;\n break;\n } catch {\n // not found — continue\n }\n }\n\n if (existingFile) {\n await addPluginToExistingConfig(path.join(cwd, existingFile));\n } else {\n await createPrettierConfig();\n }\n\n // .prettierignore\n const ignorePath = path.join(cwd, \".prettierignore\");\n let ignoreExists = false;\n try {\n await access(ignorePath);\n ignoreExists = true;\n } catch {\n // not found\n }\n if (!ignoreExists) {\n await writeFile(ignorePath, \"dist/\\nnode_modules/\\n\");\n logger.success(\"Created .prettierignore\");\n }\n }\n\n console.log(\"\");\n}\n","/**\n * 対話式プロンプトユーティリティ\n */\nimport readline from \"readline/promises\";\n\n/**\n * ユーザーにテキスト入力を求める\n */\nexport async function askInput(\n question: string,\n defaultValue?: string,\n): Promise<string> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n const suffix = defaultValue ? ` (${defaultValue})` : \"\";\n try {\n const answer = await rl.question(`${question}${suffix}: `);\n return answer.trim() || defaultValue || \"\";\n } finally {\n rl.close();\n }\n}\n\n/**\n * ユーザーに Yes/No を尋ねる\n */\nexport async function askConfirm(\n question: string,\n defaultYes: boolean = true,\n): Promise<boolean> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n const hint = defaultYes ? \"Y/n\" : \"y/N\";\n try {\n const answer = await rl.question(`${question} (${hint}): `);\n const trimmed = answer.trim().toLowerCase();\n if (trimmed === \"\") return defaultYes;\n return trimmed === \"y\" || trimmed === \"yes\";\n } finally {\n rl.close();\n }\n}\n","import { rm, access } from \"fs/promises\";\nimport path from \"path\";\nimport { loadConfig } from \"../config.js\";\nimport { handleSigint } from \"../signal-handler.js\";\nimport * as logger from \"../logger.js\";\n\nexport default async function clean(): Promise<void> {\n handleSigint();\n\n const cwd = process.cwd();\n\n let config;\n try {\n config = await loadConfig(cwd);\n } catch (e) {\n logger.errorCode(\"NS0\", (e as Error).message);\n process.exit(1);\n }\n\n const outDir = path.resolve(cwd, config.compileroptions.outdir);\n\n try {\n await access(outDir);\n } catch {\n logger.info(\n `Nothing to clean (${logger.highlight(config.compileroptions.outdir)} does not exist).`,\n );\n return;\n }\n\n await rm(outDir, { recursive: true, force: true });\n logger.success(`Deleted ${logger.highlight(config.compileroptions.outdir)}`);\n}\n","import { promises as fs } from \"fs\";\nimport path from \"path\";\nimport { spawn } from \"child_process\";\nimport convert, { diagnose } from \"../convert.js\";\nimport { loadConfig, type NsjsConfig } from \"../config.js\";\nimport { addHeader } from \"../header.js\";\nimport { handleSigint } from \"../signal-handler.js\";\nimport * as logger from \"../logger.js\";\nimport { askInput } from \"../prompt.js\";\n\ninterface RunCliOptions {\n noHeader?: boolean;\n}\n\nexport default async function run(\n file?: string,\n cliOptions: RunCliOptions = {},\n): Promise<void> {\n handleSigint();\n\n if (!file) {\n file = await askInput(\"File path\");\n if (!file) {\n logger.error(\"File path is required.\");\n process.exit(1);\n }\n }\n\n const cwd = process.cwd();\n let config: NsjsConfig;\n try {\n config = await loadConfig(cwd);\n } catch {\n config = {\n compileroptions: {\n rootdir: \"src\",\n outdir: \"dist\",\n warnuppercase: true,\n capitalizeinstrings: true,\n noheader: false,\n },\n };\n }\n\n const convertOptions = {\n capitalizeInStrings: config.compileroptions.capitalizeinstrings !== false,\n };\n\n const filePath = path.resolve(cwd, file);\n\n let code: string;\n try {\n code = await fs.readFile(filePath, \"utf-8\");\n } catch {\n logger.errorCode(\"NS2\", `File not found: ${filePath}`);\n process.exit(1);\n }\n\n // 構文エラーチェック\n const syntaxErrors = diagnose(code);\n if (syntaxErrors.length > 0) {\n const relative = path.relative(cwd, filePath).replace(/\\\\/g, \"/\");\n for (const e of syntaxErrors) {\n logger.errorCode(\n \"NS1\",\n `${relative}:${e.line}:${e.column} - ${e