#!/usr/bin/env node
/**
 * 在目标工程生成 MVFrame 推荐目录与雏形文件（views / component / api / pinia / router / config / assets）。
 *
 * 用法：
 *   node path/to/mvframe/scripts/scaffold-app.js [项目根] [--force|-f]
 *   yarn exec mvframe-init-app
 *
 * 环境变量：MVFRAME_SCAFFOLD_TARGET=/path/to/project
 *
 * 选项：
 *   --force, -f            覆盖已存在的脚手架文件
 *   --no-package-json, -n  不读不写 package.json（仅生成源码与 Vite 配置）
 */

const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
const { upsertCodexAgents } = require("./install-codex-agents.js");
const { installCursorRules } = require("./install-cursor-rules.js");

const FORCE = process.argv.includes("--force") || process.argv.includes("-f");
const NO_PKG = process.argv.includes("--no-package-json") || process.argv.includes("-n");
const argPath = process.argv.find(
  (a, i) => i >= 2 && !a.startsWith("-"),
);
const target = path.resolve(
  process.env.MVFRAME_SCAFFOLD_TARGET || argPath || process.cwd(),
);

function write(rel, content) {
  const fp = path.join(target, rel);
  fs.mkdirSync(path.dirname(fp), { recursive: true });
  if (fs.existsSync(fp) && !FORCE) {
    console.warn("[mvframe-init] 已存在，跳过（使用 --force 覆盖）:", rel);
    return;
  }
  fs.writeFileSync(fp, content, "utf8");
  console.log("[mvframe-init] 写入", rel);
}

function appendGitignoreLines(lines) {
  const fp = path.join(target, ".gitignore");
  const existing = fs.existsSync(fp) ? fs.readFileSync(fp, "utf8") : "";
  const current = new Set(
    existing
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter(Boolean),
  );
  const missing = lines.filter((line) => !current.has(line));
  if (missing.length === 0) return;
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
  fs.writeFileSync(fp, `${existing}${prefix}${missing.join("\n")}\n`, "utf8");
  console.log("[mvframe-init] 已更新 .gitignore");
}

/** 与 main.js / vite.config 模板一致；版本号与 mvframe 本仓库对齐思路，可随发布调整 */
// @vue/shared 为 Vue 3 内部包，传递依赖在部分包管理器下未提升时 Vite 解析失败，故写入直连依赖
const SCAFFOLD_DEPENDENCIES = {
  vue: "^3.5.0",
  "@vue/shared": "^3.5.0",
  "vue-router": "^4.6.0",
  pinia: "^3.0.0",
  /** mvframe 产物对 Table 列配置等直接 import，strict 包管理器下需直连依赖 */
  vuedraggable: "^4.1.0",
  mvframe: "^1.0.0",
  "element-plus": "^2.13.0",
};

const SCAFFOLD_DEV_DEPENDENCIES = {
  vite: "^6.0.0",
  "@vitejs/plugin-vue": "^5.2.0",
  "sass-embedded": "^1.97.0",
  "unplugin-auto-import": "^0.18.2",
};

const SCAFFOLD_SCRIPTS = {
  dev: "vite",
  build: "vite build",
  preview: "vite preview",
};

const VITE_DEDUPE_DEPENDENCIES = ["vue", "vue-router", "pinia"];
const VITE_CONFIG_FILES = [
  "vite.config.js",
  "vite.config.mjs",
  "vite.config.ts",
  "vite.config.mts",
];

const TEST_DINGTALK_WEBHOOK =
  "https://oapi.dingtalk.com/robot/send?access_token=142b6ddc73e8656b7906a3cb3982836a59f430a6fe5ef619a5689eed91d5f9f7";
const TEST_DINGTALK_SECRET =
  "SEC7140677a84a5190046e22d508548923dd2c4f485e38896aa0c3555ae003f4ef5";

/**
 * 合并 package.json：保留原有字段与其它依赖；仅补足脚手架所需包与启动脚本。
 * 同名依赖以目标项目已有版本为准（{ ...建议, ...已有 }）。
 * 同名 scripts 也以目标项目已有命令为准，避免覆盖自定义 dev/build。
 */
function mergePackageJson(projectRoot) {
  const pkgPath = path.join(projectRoot, "package.json");
  let pkg;

  if (fs.existsSync(pkgPath)) {
    try {
      pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
    } catch (e) {
      console.warn("[mvframe-init] package.json 解析失败，跳过合并:", e.message);
      return;
    }
  } else {
    pkg = {
      name: path.basename(projectRoot).replace(/[^a-z0-9-]/gi, "-").toLowerCase() || "mvframe-app",
      version: "0.1.0",
      private: true,
      type: "module",
      scripts: { ...SCAFFOLD_SCRIPTS },
    };
    console.log("[mvframe-init] 新建 package.json");
  }

  pkg.scripts = {
    ...SCAFFOLD_SCRIPTS,
    ...(pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {}),
  };
  pkg.dependencies = {
    ...SCAFFOLD_DEPENDENCIES,
    ...(pkg.dependencies && typeof pkg.dependencies === "object" ? pkg.dependencies : {}),
  };
  pkg.devDependencies = {
    ...SCAFFOLD_DEV_DEPENDENCIES,
    ...(pkg.devDependencies && typeof pkg.devDependencies === "object"
      ? pkg.devDependencies
      : {}),
  };

  fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
  console.log(
    "[mvframe-init] 已合并 package.json 的 scripts / dependencies / devDependencies（已有值优先）",
  );
}

function installDependencies(projectRoot) {
  console.log("[mvframe-init] 执行 yarn install");
  execFileSync("yarn", ["install"], {
    cwd: projectRoot,
    stdio: "inherit",
  });
}

function formatDedupeArray(deps) {
  return `[${deps.map((dep) => `"${dep}"`).join(", ")}]`;
}

function mergeDedupeDeps(currentBody = "") {
  const current = [...currentBody.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
  return [
    ...VITE_DEDUPE_DEPENDENCIES,
    ...current.filter((dep) => !VITE_DEDUPE_DEPENDENCIES.includes(dep)),
  ];
}

function injectViteDedupe(source) {
  const dedupeRe = /dedupe\s*:\s*\[([\s\S]*?)\]/m;
  if (dedupeRe.test(source)) {
    return source.replace(dedupeRe, (_all, body) => {
      return `dedupe: ${formatDedupeArray(mergeDedupeDeps(body))}`;
    });
  }

  const resolveRe = /resolve\s*:\s*\{/m;
  if (resolveRe.test(source)) {
    return source.replace(resolveRe, (match) => {
      return `${match}\n    dedupe: ${formatDedupeArray(VITE_DEDUPE_DEPENDENCIES)},`;
    });
  }

  const defineConfigRe = /defineConfig\s*\(\s*\{/m;
  if (defineConfigRe.test(source)) {
    return source.replace(defineConfigRe, (match) => {
      return `${match}\n  resolve: {\n    dedupe: ${formatDedupeArray(VITE_DEDUPE_DEPENDENCIES)},\n  },`;
    });
  }

  const exportDefaultObjectRe = /export\s+default\s+\{/m;
  if (exportDefaultObjectRe.test(source)) {
    return source.replace(exportDefaultObjectRe, (match) => {
      return `${match}\n  resolve: {\n    dedupe: ${formatDedupeArray(VITE_DEDUPE_DEPENDENCIES)},\n  },`;
    });
  }

  return null;
}

function findViteConfig(projectRoot) {
  return VITE_CONFIG_FILES.map((file) => path.join(projectRoot, file)).find((fp) =>
    fs.existsSync(fp),
  );
}

function ensureViteDedupe(projectRoot) {
  const fp = findViteConfig(projectRoot);
  if (!fp) {
    console.warn(
      "[mvframe-init] 未找到 vite.config.*，请手动配置 resolve.dedupe: [\"vue\", \"vue-router\", \"pinia\"]",
    );
    return;
  }

  const current = fs.readFileSync(fp, "utf8");
  const next = injectViteDedupe(current);
  if (!next) {
    console.warn(
      `[mvframe-init] 未能自动更新 ${path.basename(fp)}，请手动配置 resolve.dedupe: ["vue", "vue-router", "pinia"]`,
    );
    return;
  }
  if (next === current) {
    console.log("[mvframe-init] Vite resolve.dedupe 已包含 vue / vue-router / pinia");
    return;
  }

  fs.writeFileSync(fp, next, "utf8");
  console.log(
    `[mvframe-init] 已更新 ${path.basename(fp)}：resolve.dedupe 去重 vue / vue-router / pinia`,
  );
}

function main() {
  if (!fs.existsSync(target)) {
    console.error("[mvframe-init] 目录不存在:", target);
    process.exit(1);
  }

  const dirs = [
    "src/views/Launch",
    "src/views/Overview",
    "src/views/A",
    "src/views/B",
    "src/component",
    "src/assets/img",
    "src/assets/style",
    "src/api",
    "src/router",
    "src/pinia/chip",
    "src/config",
    "src/maps",
    "src/composition",
  ];
  for (const d of dirs) {
    fs.mkdirSync(path.join(target, d), { recursive: true });
  }

  installCursorRules(target);
  upsertCodexAgents(target);
  appendGitignoreLines([".env.local", ".env.mvframe-notify"]);

  write(
    "src/main.js",
    `import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import App from "./App.vue";
import mvframe from "mvframe";
import { pinia, store } from "mvframe/store";
import routes from "./router/routes.js";
import appConfig from "./config/index.js";
import appMaps from "./maps/index.js";
import "./assets/style/index.scss";
import "mvframe/style";
import "mvframe/style/cpt";

const app = createApp(App);
app.use(ElementPlus);

/** \`meta.public\` 为公开路由；已登录访问登录页则进首页。与 demo 一致：\`store.launch(pinia)\` 来自 \`mvframe/store\`（与插件注入的是同一单例）。 */
const launchRouteGuard = (to, from, next) => {
  if (!store.launch) {
    next();
    return;
  }
  const launch = store.launch(pinia);
  const loginPage = globalThis.$config?.loginPageName || "Entry";
  const firstPage = globalThis.$config?.firstPageName || "Overview_Home";
  if (launch.loggingOut && to.name === loginPage) {
    next();
    return;
  }
  const authed = Boolean(launch.login);
  const isPublic = to.matched.some((r) => r.meta?.public);
  if (!authed && !isPublic) {
    next({ name: loginPage, replace: true });
    return;
  }
  if (authed && to.name === loginPage) {
    next({ name: firstPage, replace: true });
    return;
  }
  next();
};

app.use(mvframe, {
  components: {
    async: ["VTable"],
  },
  vueRouter: {
    routes,
    guard: (router) => {
      router.beforeEach(launchRouteGuard);
    },
    useAdmin: true,
    adminPermission: () => Boolean(store.launch(pinia).login),
    noaccess: (to, from, next) => {
      if (from.path === "/") {
        next();
      } else {
        next({ name: "Entry" });
      }
    },
  },
  pinia: {
    useTab: true,
    storeChips: import.meta.glob("./pinia/chip/*.js", { eager: true }),
  },
  config: appConfig,
  maps: appMaps,
});
app.mount("#app");
`,
  );

  write(
    "src/App.vue",
    `<template>
  <AdminEntry v-if="isLoggedIn" />
  <Entry v-else />
</template>
<script setup>
import Entry from "./views/Launch/Entry.vue";
import AdminEntry from "./views/Launch/AdminEntry.vue";

defineOptions({
  name: "App",
  inheritAttrs: false,
});

const store = inject("store");
const launch = store.launch();

const isLoggedIn = computed(() => Boolean(launch.login));
</script>
<style lang="scss" scoped></style>
`,
  );

  write(
    "src/router/routes.js",
    `/**
 * 与 mvframe \`demo/routes.js\` 一致：单文件导出全部路由；\`meta.public\` 为公开页（见 main.js 守卫）。
 * B 嵌套使用 Layout + 子路由；日后若按权限裁剪菜单，可对本数组 \`filter\` 后再交给 Frame。
 */
export default [
  {
    path: "/",
    name: "Entry",
    component: () => import("@/views/Launch/LoginPage.vue"),
    meta: {
      title: "Login",
      public: true,
    },
  },
  {
    path: "/overview",
    name: "Overview_Home",
    component: () => import("@/views/Overview/Home.vue"),
    meta: {
      title: "Overview",
      admin: true,
    },
  },
  {
    path: "/a",
    name: "A_Home",
    component: () => import("@/views/A/Home.vue"),
    meta: {
      title: "A",
      icon: "im-swap-right",
      admin: true,
    },
  },
  {
    path: "/b",
    name: "B_Home",
    component: () => import("@/views/B/Layout.vue"),
    meta: {
      title: "B",
      icon: "im-swap-right",
      admin: true,
    },
    children: [
      {
        path: "first",
        name: "B_First",
        component: () => import("@/views/B/First.vue"),
        meta: { title: "B_First" },
      },
      {
        path: "second",
        name: "B_Second",
        component: () => import("@/views/B/Second.vue"),
        meta: { title: "B_Second" },
      },
      {
        path: "third",
        name: "B_Third",
        component: () => import("@/views/B/Third.vue"),
        meta: { title: "B_Third" },
      },
      {
        path: "fourth",
        name: "B_Fourth",
        component: () => import("@/views/B/Fourth.vue"),
        meta: { title: "B_Fourth" },
      },
      {
        path: "fifth",
        name: "B_Fifth",
        component: () => import("@/views/B/Fifth.vue"),
        meta: { title: "B_Fifth" },
      },
    ],
  },
];
`,
  );

  write(
    "src/config/index.js",
    `/**
 * 与 mvframe 内置 config 合并 → globalThis.$config
 * @type {Record<string, unknown>}
 */
export default {
  iconfont: {
    // url: "//at.alicdn.com/t/c/your_font.js",
    // prefix: "ant",
  },
  notify: {
    enabled: true,
    endpoint: import.meta.env.VITE_MVFRAME_NOTIFY_ENDPOINT || "http://127.0.0.1:3300",
    token: import.meta.env.VITE_MVFRAME_NOTIFY_TOKEN || "",
    timeout: 5000,
  },
  table: {
    summaryMetric: null,
  },
};
`,
  );

  write(
    "src/maps/index.js",
    `/**
 * 与 mvframe 内置 maps/chip/*.js 深度合并（不挂全局），供 useMap / getMaps / patchMaps 使用。
 * 顶层键与芯片文件名一致（如 common、asa）；子键用 useMap("common.vTableTool.refresh") 读取。
 * @type {Record<string, unknown>}
 */
export default {
  // asa: { countryAll: { obj: {} } },
};
`,
  );

  write(
    "src/pinia/chip/app.js",
    `/** 业务 Pinia chip：路径须为 pinia/chip/*.js，export default { state, actions } */
const state = () => ({
  // demo: null,
});

const actions = {
  saveData(key, value) {
    this[key] = value;
  },
};

export default {
  state,
  actions,
};
`,
  );

  write(
    "src/pinia/chip/launch.js",
    `/** 登录态：与 demo/pinia/launch.js、main.js launchRouteGuard、LoginPage 一致 */
const readLoginFromStorage = () => {
  try {
    const raw = localStorage.getItem("login");
    if (raw == null) return false;
    return raw === "true" || JSON.parse(raw) === true;
  } catch {
    return false;
  }
};

export default {
  state: () => ({
    userinfo: {},
    login: readLoginFromStorage(),
    loggingOut: false,
  }),
  actions: {
    /** 登录成功：合并写入 userinfo，并置 login: true */
    setLogin(payload = {}) {
      this.userinfo = {
        ...this.userinfo,
        ...payload,
      };
      this.login = true;
      localStorage.setItem("login", true);
    },
    clearLogin() {
      this.userinfo = {};
      this.login = false;
      this.loggingOut = false;
      localStorage.removeItem("login");
    },
  },
};
`,
  );

  write(
    "src/views/Launch/Entry.vue",
    `<template>
  <router-view />
</template>
<script setup>
defineOptions({
  name: "LaunchEntry",
  inheritAttrs: false,
});
</script>
`,
  );

  write(
    "src/views/Launch/AdminEntry.vue",
    `<template>
  <Frame class="App wp100 vh100" :menu="frameMenu" :page="{}">
    <template #logo>
      <span class="logo-placeholder">Logo</span>
    </template>
    <template #logomini>
      <span class="logo-mini">L</span>
    </template>
  </Frame>
</template>
<script setup>
import routes from "@/router/routes.js";

defineOptions({
  name: "AdminEntry",
  inheritAttrs: false,
});

const frameMenu = {
  iconClass: "imicon",
  routes: routes.filter((r) => r.meta?.public !== true),
};
</script>
<style lang="scss" scoped>
.logo-placeholder {
  display: inline-block;
  min-width: 7.5rem;
  padding: 0.25rem 0.5rem;
  border-radius: 0.25rem;
  background: var(--color-primary, #16b1ff);
  color: #fff;
  font-size: 0.875rem;
}
.logo-mini {
  display: inline-flex;
  width: 1.875rem;
  height: 1.875rem;
  align-items: center;
  justify-content: center;
  border-radius: 0.25rem;
  background: var(--color-green, #20c997);
  color: #fff;
  font-size: 0.75rem;
}
</style>
`,
  );

  write(
    "src/views/Launch/LoginPage.vue",
    `<template>
  <div class="LoginPage wp100 vh100">
    <Login
      :form-fileds="formFileds"
      :login-methods="[0]"
      :loading="loading"
      @submit="onSubmit"
    />
  </div>
</template>
<script setup>
defineOptions({
  name: "LoginPage",
  inheritAttrs: false,
});

const router = useRouter();
const store = inject("store");
const launch = store.launch();
const loading = ref(false);

const onSubmit = async (payload) => {
  loading.value = true;
  try {
    launch.setLogin(payload ?? {});
    await router.replace({ name: "Overview_Home" });
  } catch {
    /* 路由失败时仍结束 loading */
  } finally {
    loading.value = false;
  }
};

/** 与 mvframe demo 一致：自定义提交字段名 account / pwd */
const formFileds = {
  username: { valueKey: "account" },
  password: { valueKey: "pwd" },
};
</script>
<style lang="scss" scoped>
.LoginPage {
  box-sizing: border-box;
}
</style>
`,
  );

  write(
    "src/api/index.js",
    `/**
 * 接口聚合：按需拆分 chip 并在业务中 import
 * 例：export * from "./chip/user.js";
 */
export {};
`,
  );

  write(
    "src/assets/style/index.scss",
    `/* 项目全局样式入口（main.js 已 import 本文件） */
/* MVFrame：main.js 已 import "mvframe/style" 与 "mvframe/style/cpt"（发布包 CSS），勿在此处再 @import 同一路径，避免重复。 */
/* 若需在业务 SCSS 内复用 mvframe 断点 / mixin，直接：@use "mvframe/style/mixin" as *; */
/* 例如：@include media-down(sm) { ... } */

body {
  margin: 0;
}
`,
  );

  write(
    "src/views/Overview/Home.vue",
    `<template>
  <Page title="Overview" subtitle="登录后默认首页，可替换为业务概览">
    <p>
      路由与 mvframe <code>demo/routes.js</code> 对齐：<code>/overview</code>、<code>/a</code>、<code>/b/...</code>；公开入口为
      <code>routes.js</code> 中的 <code>Entry</code>。
    </p>
  </Page>
</template>
<script setup>
defineOptions({
  name: "OverviewHome",
  inheritAttrs: false,
});
</script>
<style lang="scss" scoped>
code {
  font-size: 0.875rem;
}
</style>
`,
  );

  write(
    "src/views/A/Home.vue",
    `<template>
  <Page title="A" subtitle="一级业务模块示例">
    <p>对应路由 <code>A_Home</code>，路径 <code>/a</code>。</p>
  </Page>
</template>
<script setup>
defineOptions({
  name: "AHome",
  inheritAttrs: false,
});
</script>
<style lang="scss" scoped>
code {
  font-size: 0.875rem;
}
</style>
`,
  );

  write(
    "src/views/B/Layout.vue",
    `<template>
  <Page title="B" subtitle="嵌套路由父级，子页在下方出口渲染">
    <router-view />
  </Page>
</template>
<script setup>
defineOptions({
  name: "BLayout",
  inheritAttrs: false,
});
</script>
`,
  );

  const bSubPages = [
    ["First", "B_First"],
    ["Second", "B_Second"],
    ["Third", "B_Third"],
    ["Fourth", "B_Fourth"],
    ["Fifth", "B_Fifth"],
  ];
  for (const [file, routeTitle] of bSubPages) {
    write(
      `src/views/B/${file}.vue`,
      `<template>
  <Page title="${routeTitle}" subtitle="B 模块子路由">
    <p>组件 <code>${file}</code>，路由名 <code>${routeTitle}</code>。</p>
  </Page>
</template>
<script setup>
defineOptions({
  name: "B${file}",
  inheritAttrs: false,
});
</script>
<style lang="scss" scoped>
code {
  font-size: 0.875rem;
}
</style>
`,
    );
  }

  write("src/component/.gitkeep", "");
  write("src/assets/img/.gitkeep", "");
  write("src/composition/.gitkeep", "");

  const idxHtml = path.join(target, "index.html");
  if (!fs.existsSync(idxHtml) || FORCE) {
    write(
      "index.html",
      `<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
`,
    );
  }

  const existingViteConfig = findViteConfig(target);
  const viteConfigRel = existingViteConfig
    ? path.relative(target, existingViteConfig)
    : "vite.config.js";
  if (!existingViteConfig || FORCE) {
    write(
      viteConfigRel,
      `import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
import { fileURLToPath } from "url";
import AutoImport from "unplugin-auto-import/vite";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      include: [/\\.vue$/, /\\.vue\\?vue/, /\\.js$/],
      imports: [
        "vue",
        "vue-router",
        {
          "mvframe/maps": [
            "useMap",
            "getMaps",
            "patchMaps",
            "mapLang",
            "mapLangPath",
          ],
        },
      ],
      vueTemplate: true,
      dts: true,
    }),
  ],
  resolve: {
    dedupe: ["vue", "vue-router", "pinia"],
    alias: {
      "@": path.resolve(__dirname, "src"),
      "@cps": path.resolve(__dirname, "src/composition"),
    },
  },
  server: {
    port: 5173,
  },
});
`,
    );
  }
  ensureViteDedupe(target);

  write(
    ".env.mvframe-notify.example",
    `# Copy to .env.mvframe-notify for the local Node notify service.
# Do not put DingTalk webhook/secret in src/main.js or other browser-bundled files.
# The DingTalk values below are MVFrame's test robot config. Replace them for production.

MVFRAME_NOTIFY_PORT=3300
MVFRAME_NOTIFY_HOST=127.0.0.1
MVFRAME_NOTIFY_TOKEN=
MVFRAME_NOTIFY_TIMEOUT_MS=5000
MVFRAME_NOTIFY_CORS_ORIGIN=*

DINGTALK_WEBHOOK="${TEST_DINGTALK_WEBHOOK}"
DINGTALK_SECRET="${TEST_DINGTALK_SECRET}"
DINGTALK_AT_MOBILES=""
DINGTALK_AT_USER_IDS=""
DINGTALK_AT_ALL=""
`,
  );

  write(
    ".env.mvframe-notify",
    `# Local MVFrame DingTalk notify service config.
# This file is generated for test use and is ignored by git.
# Replace the DingTalk webhook/secret before production use.

MVFRAME_NOTIFY_PORT=3300
MVFRAME_NOTIFY_HOST=127.0.0.1
MVFRAME_NOTIFY_TOKEN=
MVFRAME_NOTIFY_TIMEOUT_MS=5000
MVFRAME_NOTIFY_CORS_ORIGIN=*

DINGTALK_WEBHOOK="${TEST_DINGTALK_WEBHOOK}"
DINGTALK_SECRET="${TEST_DINGTALK_SECRET}"
DINGTALK_AT_MOBILES=""
DINGTALK_AT_USER_IDS=""
DINGTALK_AT_ALL=""
`,
  );

  write(
    ".env.local.example",
    `# Copy to .env.local when you need the browser app to call mvframe-notify.
# VITE_MVFRAME_NOTIFY_TOKEN should match MVFRAME_NOTIFY_TOKEN in .env.mvframe-notify.

VITE_MVFRAME_NOTIFY_ENDPOINT=http://127.0.0.1:3300
VITE_MVFRAME_NOTIFY_TOKEN=
`,
  );

  write(
    "MVFRAME-SCAFFOLD.md",
    `# MVFrame 雏形已生成

## 依赖

初始化脚本会**合并**（不整文件覆盖）\`package.json\` 的 \`scripts\` / \`dependencies\` / \`devDependencies\`：**已声明的脚本和包保留你的原值**，仅补上缺失项，并自动执行 \`yarn install\`。然后执行：

\`\`\`bash
yarn dev
\`\`\`

如需同时启动 Vite 与 MVFrame 钉钉通知服务，可显式执行框架命令（与 \`mvframe-b\` 类似由 MVFrame 包提供，不会自动覆盖宿主脚本）：

\`\`\`bash
yarn exec mvframe-d -n
\`\`\`

若需跳过对 \`package.json\` 的修改：\`node scripts/scaffold-app.js --no-package-json\` 或 \`node scripts/scaffold-app.js -n\`。

自动生成的 \`vite.config.js\` 已包含 \`unplugin-auto-import\`；\`dts: true\` 时类型默认写在项目根 \`auto-imports.d.ts\`。初始化脚本还会确保宿主 Vite 配置里存在 \`resolve.dedupe: ["vue", "vue-router", "pinia"]\`，避免宿主项目与 \`mvframe\` 各自解析出多份 Vue / Router / Pinia 实例。

\`src/main.js\` 默认在 \`app.use(mvframe, { components: { async: ["VTable"] } })\` 中异步注册 \`VTable\`。这样只有页面实际渲染 \`<VTable>\` 时才加载 VisActor 相关 chunk，避免 \`@visactor/vtable\` 进入宿主首屏同步链。

## Launch（登录壳）

雏形与 **mvframe demo** 对齐：\`pinia/chip/launch.js\`（顶层 \`login\` + \`userinfo\`）、\`App.vue\`（\`<AdminEntry v-if />\` / \`<Entry v-else />\`）、\`main.js\` 内 \`import { store, pinia } from "mvframe/store"\` + \`launchRouteGuard\` + \`useAdmin\` / \`adminPermission\` / \`noaccess\`。未登录仅 \`meta.public\`；已登录进 \`Entry\` 会重定向到 \`Overview_Home\`（\`/overview\`）。若不需门禁，将 \`useAdmin\` 改为 \`false\` 并删除 \`adminPermission\` 与 \`noaccess\`。

## 路由（与 mvframe \`demo/routes.js\` 对齐）

| 路由名 | 路径 | 说明 |
|--------|------|------|
| \`Entry\` | \`/\` | 登录，\`meta.public\` |
| \`Overview_Home\` | \`/overview\` | 登录后默认页 |
| \`A_Home\` | \`/a\` | 一级模块示例 |
| \`B_Home\` | \`/b\` | 嵌套父级（\`Layout.vue\` + \`router-view\`） |
| \`B_First\` … \`B_Fifth\` | \`/b/first\` … | 子路由 |

路由集中在 \`src/router/routes.js\`（与 demo 单文件一致）；\`Frame\` 的 \`menu.routes\` 由该表筛选掉 \`meta.public\` 后与注册同源。

### 报错 \`Could not resolve '@vue/shared'\`

这是 **Vue 3 自带的底层包**，一般不必手写；在 **pnpm / 严格 node_modules** 等环境下可能未被提升到可被 Vite 解析的位置。脚手架已在 \`dependencies\` 中合并 \`@vue/shared\`（与 \`vue\` 同主版本）；老项目可手动执行 \`yarn add @vue/shared@^3.5\`。

### 报错无法解析 \`vuedraggable\`

发布包 \`dist/index.js\` 会对 \`vuedraggable\` 做 **直连 import**（如 Table 列配置）。脚手架已在 \`dependencies\` 中写入 \`vuedraggable@^4.1.0\`；老项目可 \`yarn add vuedraggable@^4.1\`。

## 样式

\`src/main.js\` 已包含 \`import "mvframe/style"\` 与 \`import "mvframe/style/cpt"\`（分别对应工具类/变量与 **Mvc\*** 组件 scoped 等样式），一般 **无需** 在 \`index.scss\` 再引一遍 mvframe 的 dist CSS，以免重复。项目级覆盖写在 \`src/assets/style/index.scss\` 即可。

宿主项目如需在 SCSS 内复用 MVFrame 断点，直接：

\`\`\`scss
@use "mvframe/style/mixin" as *;

.Panel {
  padding: 1.5rem;

  @include media-down(sm) {
    padding: 1rem;
  }
}
\`\`\`

发布包的 \`mvframe/style\` 还额外提供响应式工具类：\`{断点}-{up|down|only}-{工具类}\`，如 \`md-down-hide\`、\`lg-up-flexMode\`、\`xs-only-block\`。适合直接写在 template 上；复杂宽高/间距调整仍建议在业务 SCSS 里配合 media mixin 写。

做 Grid 列数切换时，也可以直接在容器上写断点列数类，例如：

\`\`\`html
<div class="grid col2 md2 lg3 sm1 g16"></div>
\`\`\`

表示默认 2 列，\`sm\` 1 列，\`md\` 2 列，\`lg\` 3 列。

## 表单 Label

\`Input\`、\`Textarea\`、\`Select\`、\`SelectV2\` 支持统一的 label 接口：

\`\`\`vue
<Input v-model="form.name" label="Name" />
<Input v-model="form.name" label="Material Name" material-label />

<Textarea v-model="form.remark" label="Remark" />
<Textarea v-model="form.remark" label="Material Remark" material-label />

<Select v-model="form.type" :options="options" label="Type" />
<Select v-model="form.type" :options="options" label="Material Type" material-label />
\`\`\`

规则：

- 只传 \`label\`：渲染普通上方标题
- 传 \`label\` + \`material-label\`：渲染 Material 风格浮动标签
- 不传 \`label\`：保持组件默认外观

Material 风格 label 的公共壳样式统一在 \`src/style/chip/element.scss\`。如果你要做 Material 风格表单，优先直接使用 \`label\` + \`material-label\`，不要在业务侧再重复写一套浮动标题。

## 子路径（按需）

发布包 \`exports\` 还提供 \`mvframe/composition\`、\`mvframe/util\`、\`mvframe/store\`、\`mvframe/notify\` 等，业务侧可 \`import { ... } from "mvframe/composition"\` 等，参见包内 \`package.json\` 的 \`exports\`。

## 钉钉通知（mvframe-notify）

脚手架已初始化：

- \`src/config/index.js\` 的 \`notify\` 节点：浏览器端只保存本地服务地址和访问 token。
- \`.env.mvframe-notify\` 与 \`.env.mvframe-notify.example\`：已写入 MVFrame 测试钉钉机器人配置，生产环境请替换 \`DINGTALK_WEBHOOK\` / \`DINGTALK_SECRET\`。
- \`.env.local.example\`：复制为 \`.env.local\`，填写 \`VITE_MVFRAME_NOTIFY_ENDPOINT\` / \`VITE_MVFRAME_NOTIFY_TOKEN\`。

启动通知服务：

\`\`\`bash
yarn exec mvframe-notify
\`\`\`

同时启动 Vite 和通知服务：

\`\`\`bash
yarn exec mvframe-d -n
\`\`\`

前端发送：

\`\`\`js
await globalThis.$notify.send("需要发送到钉钉的消息");
\`\`\`

或按需导入：

\`\`\`js
import { notify } from "mvframe/notify";

await notify("需要发送到钉钉的消息");
\`\`\`

注意：钉钉 webhook / secret 不应写进 \`main.js\`、\`src/config/index.js\` 或任何会被 Vite 打包到浏览器的文件。

## 目录约定

| 路径 | 用途 |
|------|------|
| \`src/views\` | 主要页面（模块大驼峰 + \`Home.vue\`，见 mvframe views 规范） |
| \`src/component\` | 项目级公共组件（与 mvframe 全局 \`Mvc*\` 区分） |
| \`src/api\` | 接口 |
| \`src/assets/style\` | 样式入口，main.js 已 import |
| \`src/assets/img\` | 静态图 |
| \`src/router/routes.js\` | 全部路由（与 \`demo/routes.js\` 结构一致）→ \`vueRouter.routes\` |
| \`src/views/Launch/*\` | 登录壳与登录页 |
| \`src/views/Overview\`、\`A\`、\`B\` | 与路由表一一对应的页面 |
| \`src/pinia/chip/*.js\` | 业务 store → \`storeChips: import.meta.glob(...)\` |
| \`src/config/index.js\` | 合并进 \`globalThis.$config\` |
| \`src/maps/index.js\` | 与库内置 \`maps/chip\` 深度合并（模块内）；\`useMap().$l()\` / \`mapLangPath\` / \`mapLang\` 可由 auto-import 从 \`mvframe/maps\` 引入 |
| \`src/composition\` | 可选，与 Vite 别名 \`@cps\` 对应 |

## Codex 规则（\`AGENTS.md\`）

初始化脚本会写入/更新项目根 **\`AGENTS.md\`** 的 MVFrame 区块，供 Codex 在宿主项目内优先使用 **MVFrame 全局组件、全局方法与全局样式工具类**，并要求 **每次 AI 完成开发后调用 \`yarn exec mvframe-notify --once --message "..."\` 发送完成通知**。该区块带有 \`MVFRAME-CODEX-RULES\` 标记，重复执行脚手架会更新这段内容，不会覆盖你在 \`AGENTS.md\` 中的其它规则。

同时初始化 Codex + Cursor 两侧规则：

\`\`\`bash
yarn exec mvframe-init-rules
\`\`\`

仅安装 Codex 规则：

\`\`\`bash
yarn exec mvframe-install-codex-rules
\`\`\`

## Cursor 规则（\`.cursor/rules\`）

初始化脚本会把 **mvframe 包内**与仓库一致的 **\`*.mdc\`** 补充到目标项目 **\`/.cursor/rules/\`**（\`component-hierarchy\`、\`script-setup\`、\`style-system\`、\`views\`、\`router\`、\`global-components\`、\`data\`、\`util\`）。若宿主项目已存在同名规则文件，则保留宿主文件，仅补缺失项。

仅安装 Cursor 规则：

\`\`\`bash
yarn exec mvframe-install-cursor-rules
\`\`\`

npm 包需在 \`files\` 中包含 \`.cursor/rules\`；使用本地 \`file:\` / 源码链接时同样可用。

## Cursor Skill（可选）

\`\`\`bash
yarn exec mvframe-install-cursor-skill
\`\`\`
`,
  );

  if (!NO_PKG) {
    mergePackageJson(target);
    installDependencies(target);
  }

  console.log("[mvframe-init] 完成。请阅读 MVFRAME-SCAFFOLD.md 并在项目根执行 yarn dev。");
}

main();
