create-next-imagicma 0.1.6 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,12 +25,15 @@ node ./create-next-imagicma/bin/create-next-imagicma.mjs demo-next --template ne
25
25
  ## 参数
26
26
 
27
27
  - `--template <hono|next>`:选择模板。默认 `hono`。
28
- - `--port <number>`:设置新项目默认端口。
29
- - `next` 模板:写入 `next dev/start -p <port>`
30
- - `hono` 模板:写入 `/.imagicma/port.json`,并同步 `process-compose.yaml` 的 `PORT`
28
+ - `--port <number>`:写入项目运行时端口文件 `.imagicma/runtime.env`,内容为 `PORT=<number>`。
31
29
  - `--theme <name>`:设置默认主题(`quadratic`、`nomad`、`honey`、`zen-garden`、`highlighter`)。
32
30
  - `-v, --version`:显示版本号。
33
31
 
32
+ 说明:
33
+
34
+ - `.imagicma/runtime.env` 是运行时文件,不是业务配置文件。
35
+ - 脚手架不会写入 `port.json`,模板运行时只认大写 `PORT`。
36
+
34
37
  ## 依赖安装策略
35
38
 
36
39
  - 优先 `pnpm install`
@@ -40,7 +40,7 @@ function printHelp() {
40
40
 
41
41
  参数:
42
42
  --template <name> 选择模板(可选:${TEMPLATE_CHOICES.join("、")},默认:${DEFAULT_TEMPLATE})
43
- --port <number> 设置新项目默认端口
43
+ --port <number> 写入项目运行时端口文件 .imagicma/runtime.env
44
44
  --theme <name> 设置默认主题(可选:${THEME_STYLES.join(", ")})
45
45
  -h, --help 显示帮助
46
46
  -v, --version 显示版本号
@@ -202,7 +202,7 @@ async function ensureTargetDirReady(targetDir) {
202
202
  }
203
203
  }
204
204
 
205
- async function updatePackageName(targetDir, port, template) {
205
+ async function updatePackageName(targetDir, template) {
206
206
  const pkgPath = path.join(targetDir, "package.json");
207
207
  const raw = await fs.readFile(pkgPath, "utf8");
208
208
  const pkg = JSON.parse(raw);
@@ -210,14 +210,10 @@ async function updatePackageName(targetDir, port, template) {
210
210
  pkg.name = sanitizePackageName(path.basename(targetDir));
211
211
 
212
212
  if (template === "next") {
213
- const devScript = port === undefined ? "next dev" : `next dev -p ${port}`;
214
- const startScript =
215
- port === undefined ? "next start" : `next start -p ${port}`;
216
-
217
213
  pkg.scripts = {
218
214
  ...(pkg.scripts ?? {}),
219
- dev: devScript,
220
- start: startScript,
215
+ dev: "next dev",
216
+ start: "next start",
221
217
  };
222
218
  } else {
223
219
  pkg.scripts = {
@@ -228,57 +224,6 @@ async function updatePackageName(targetDir, port, template) {
228
224
 
229
225
  await fs.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
230
226
  }
231
-
232
- async function updateProcessComposePort(targetDir, port) {
233
- if (port === undefined) return;
234
-
235
- const filePath = path.join(targetDir, "process-compose.yaml");
236
-
237
- let raw;
238
- try {
239
- raw = await fs.readFile(filePath, "utf8");
240
- } catch (error) {
241
- if (error && typeof error === "object" && error.code === "ENOENT") {
242
- return;
243
- }
244
- throw error;
245
- }
246
-
247
- const next = raw.replace(
248
- /^(\s*-\s*["']?PORT=)\d+(["']?\s*)$/m,
249
- `$1${port}$2`,
250
- );
251
- await fs.writeFile(filePath, next);
252
- }
253
-
254
- async function updateLockedPortFile(targetDir, port) {
255
- if (port === undefined) return;
256
-
257
- const filePath = path.join(targetDir, ".imagicma", "port.json");
258
-
259
- let current = {};
260
- try {
261
- const raw = await fs.readFile(filePath, "utf8");
262
- const parsed = JSON.parse(raw);
263
- if (parsed && typeof parsed === "object") {
264
- current = parsed;
265
- }
266
- } catch (error) {
267
- if (!(error && typeof error === "object" && error.code === "ENOENT")) {
268
- throw error;
269
- }
270
- }
271
-
272
- const next = {
273
- ...current,
274
- port,
275
- locked: true,
276
- version: 1,
277
- };
278
- await fs.mkdir(path.dirname(filePath), { recursive: true });
279
- await fs.writeFile(filePath, `${JSON.stringify(next, null, 2)}\n`);
280
- }
281
-
282
227
  async function updateDefaultTheme(targetDir, theme) {
283
228
  if (theme === undefined) return;
284
229
 
@@ -296,6 +241,18 @@ async function updateDefaultTheme(targetDir, theme) {
296
241
  await fs.writeFile(filePath, next);
297
242
  }
298
243
 
244
+ async function writeRuntimeEnv(targetDir, port) {
245
+ const runtimeEnvPath = path.join(targetDir, ".imagicma", "runtime.env");
246
+
247
+ if (port === undefined) {
248
+ await fs.rm(runtimeEnvPath, { force: true });
249
+ return;
250
+ }
251
+
252
+ await fs.mkdir(path.dirname(runtimeEnvPath), { recursive: true });
253
+ await fs.writeFile(runtimeEnvPath, `PORT=${port}\n`);
254
+ }
255
+
299
256
  function runCommand(command, args, options) {
300
257
  return new Promise((resolve, reject) => {
301
258
  const child = spawn(command, args, { stdio: "inherit", ...options });
@@ -417,12 +374,9 @@ async function main() {
417
374
  // ignore
418
375
  }
419
376
 
420
- await updatePackageName(targetDir, port, template);
421
- await updateProcessComposePort(targetDir, port);
422
- if (template === "hono") {
423
- await updateLockedPortFile(targetDir, port);
424
- }
377
+ await updatePackageName(targetDir, template);
425
378
  await updateDefaultTheme(targetDir, theme);
379
+ await writeRuntimeEnv(targetDir, port);
426
380
 
427
381
  await initGit(targetDir);
428
382
 
@@ -442,7 +396,7 @@ async function main() {
442
396
  console.log(`\n✅ 项目已创建:${targetDir}`);
443
397
  console.log(`模板类型:${template}`);
444
398
  if (port !== undefined) {
445
- console.log(`已设置默认端口:${port}`);
399
+ console.log(`已写入运行时端口文件:.imagicma/runtime.env (PORT=${port})`);
446
400
  }
447
401
  if (theme !== undefined) {
448
402
  console.log(`已设置默认主题:${theme}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-next-imagicma",
3
- "version": "0.1.6",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-next-imagicma": "./bin/create-next-imagicma.mjs"
@@ -49,6 +49,13 @@
49
49
  - `run_test` 卡住或无进展时要快速失败并给出证据,禁止长时间空转重试。
50
50
  - 不允许“口头完成”。完成声明必须有工具证据支撑。
51
51
  - SQLite 路径必须保持单一 schema 真相源,禁止手写临时 SQL 与 ORM schema 并行漂移。
52
+ - 禁止修改 `scripts/` 下的受保护启动文件:`imagicma-common.mjs`、`imagicma-guard.mjs`、`imagicma-dev.mjs`、`imagicma-start.mjs`、`imagicma-runtime-logs.mjs`。
53
+ - 禁止修改 `package.json` 中 `scripts.dev` 与 `scripts.start`(以及对应 `predev`、`prestart`)命令。
54
+ - 禁止直接执行 `vite` 或 `node dist/server/index.js` 或 `pnpm dev` 或 `pnpm start` 启动项目;只能通过 `restart_workflow` 启动。
55
+ - 禁止主动注入环境变量到 `process.env`。
56
+ - 端口契约只允许使用大写 `PORT`。
57
+ - 运行时端口读取顺序固定为:外部环境变量 `PORT` -> 项目内 `.imagicma/runtime.env`。
58
+ - 禁止写入 `/.imagicma/port.json`,禁止在 `process-compose.yaml` 中固化端口真相源。
52
59
 
53
60
  ## 状态文件写入规则
54
61
 
@@ -59,7 +66,8 @@
59
66
  ## UI 质量门禁(必须全部满足)
60
67
 
61
68
  - `/` 路由必须可访问并落到首页组件。
62
- - 接入真实业务时,应将 `/` 首页替换为业务内容,不要长期保留模板默认文案。
69
+ - `client/src/pages/home.tsx` 不允许为空文件、空组件,或仅返回 `null` / 空白占位;首页必须渲染可见内容。
70
+ - 接入真实业务时,应将 `/` 首页替换为真实业务内容,不要长期保留模板默认文案或空白首页。
63
71
  - 视觉风格必须明确:字体、颜色、间距、动效要统一。
64
72
  - 必须定义可复用设计 token(颜色、圆角、阴影、间距等级)。
65
73
  - 至少覆盖桌面与移动端关键断点,不允许内容溢出。
@@ -26,7 +26,18 @@ pnpm install
26
26
  pnpm dev
27
27
  ```
28
28
 
29
- 默认端口由 `/.imagicma/port.json`。
29
+ 启动时按以下顺序解析端口:
30
+
31
+ 1. 显式环境变量 `PORT`
32
+ 2. 项目内 `.imagicma/runtime.env`
33
+
34
+ 如果两者都不存在,启动会直接失败。
35
+ 项目内唯一允许的端口文件是 `.imagicma/runtime.env`,内容示例:
36
+
37
+ ```bash
38
+ PORT=6424
39
+ ```
40
+
30
41
  请勿直接执行 `vite` 启动,必须使用 `pnpm dev`。
31
42
 
32
43
  ## 构建与运行
@@ -40,7 +51,7 @@ pnpm start
40
51
  - 前端:`dist/client`
41
52
  - 后端:`dist/server`
42
53
  - `start`:由受保护脚本启动服务(禁止直接 `node dist/server/index.js`),并由 Hono 承载 API + 静态资源 + SPA fallback。
43
- - 生产端口同样由 `/.imagicma/port.json` 锁定,不接受 `PORT` 覆盖。
54
+ - 生产启动同样按 `PORT -> .imagicma/runtime.env` 的顺序解析端口。
44
55
 
45
56
  ## 数据库
46
57
 
@@ -53,6 +64,11 @@ pnpm db:push
53
64
  - 默认数据库文件:`./.data/app.db`
54
65
  - 如需自定义位置,可复制 `.env.example` 为 `.env.local` 后设置 `DATABASE_FILE`
55
66
 
67
+ ## 运行时文件
68
+
69
+ - `.imagicma/runtime.env`:项目运行端口契约,只使用大写 `PORT`
70
+ - `/.imagicma/port.json`:已废弃,不再使用
71
+
56
72
  ## 验证路径
57
73
 
58
74
  - API:`GET /api/greeting`
@@ -7,14 +7,14 @@ import { cva, type VariantProps } from "class-variance-authority"
7
7
  import { cn } from "@/lib/utils"
8
8
 
9
9
  const buttonVariants = cva(
10
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold tracking-[0.01em] transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
10
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold tracking-[0.01em] transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover-elevate active-elevate-2",
11
11
  {
12
12
  variants: {
13
13
  variant: {
14
14
  default:
15
- "bg-primary text-primary-foreground border border-primary/45 shadow-[0_10px_24px_hsl(var(--primary)/0.38)] hover:-translate-y-0.5 hover:shadow-[0_14px_30px_hsl(var(--primary)/0.46)] active:translate-y-0",
15
+ "bg-primary text-primary-foreground border border-primary/45",
16
16
  destructive:
17
- "bg-destructive text-destructive-foreground border border-destructive/45 shadow-[0_10px_22px_hsl(var(--destructive)/0.3)] hover:brightness-105",
17
+ "bg-destructive text-destructive-foreground border border-destructive/45 hover:brightness-105",
18
18
  outline:
19
19
  "border border-input/80 bg-white/75 text-foreground shadow-[0_4px_12px_rgba(15,23,42,0.08)] hover:bg-white hover:border-ring/35",
20
20
  secondary: "border border-secondary-border bg-secondary/90 text-secondary-foreground shadow-sm hover:bg-secondary",
@@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
13
13
  <CheckboxPrimitive.Root
14
14
  ref={ref}
15
15
  className={cn(
16
- "peer h-5 w-5 shrink-0 rounded-full border-2 border-primary/45 bg-white shadow-[0_1px_3px_rgba(15,23,42,0.18)] ring-offset-background transition-[background-color,border-color,box-shadow] duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:shadow-[0_8px_16px_hsl(var(--primary)/0.35)]",
16
+ "peer h-5 w-5 shrink-0 rounded-full border-2 border-primary/45 bg-white shadow-[0_1px_3px_rgba(15,23,42,0.18)] ring-offset-background transition-[background-color,border-color,box-shadow] duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:shadow-[0_8px_16px_rgba(15,23,42,0.18)]",
17
17
  className
18
18
  )}
19
19
  {...props}
@@ -0,0 +1,55 @@
1
+ const PROD_PARENT_ORIGINS = new Set(["https://agentma.cn", "https://imagicma.cn"]);
2
+ const LOCAL_PARENT_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i;
3
+ const LOCAL_IMAGICMA_PARENT_RE = /^https?:\/\/([a-z0-9-]+\.)?local\.(agentma\.cn|imagicma\.cn)(:\d+)?$/i;
4
+
5
+ type PreviewBridgeState = {
6
+ parentOrigin: string | null;
7
+ };
8
+
9
+ declare global {
10
+ interface Window {
11
+ __IMAGICMA_PREVIEW_BRIDGE__?: PreviewBridgeState;
12
+ }
13
+ }
14
+
15
+ function getBridgeState(): PreviewBridgeState {
16
+ if (typeof window === "undefined") {
17
+ return { parentOrigin: null };
18
+ }
19
+ if (!window.__IMAGICMA_PREVIEW_BRIDGE__) {
20
+ window.__IMAGICMA_PREVIEW_BRIDGE__ = {
21
+ parentOrigin: null,
22
+ };
23
+ }
24
+ return window.__IMAGICMA_PREVIEW_BRIDGE__;
25
+ }
26
+
27
+ export function isAllowedPreviewParentOrigin(origin: string): boolean {
28
+ if (!origin) return false;
29
+ return PROD_PARENT_ORIGINS.has(origin) || LOCAL_PARENT_RE.test(origin) || LOCAL_IMAGICMA_PARENT_RE.test(origin);
30
+ }
31
+
32
+ export function bindPreviewParentOrigin(origin: string | null): string | null {
33
+ const state = getBridgeState();
34
+ if (!origin) {
35
+ state.parentOrigin = null;
36
+ return state.parentOrigin;
37
+ }
38
+ if (!isAllowedPreviewParentOrigin(origin)) {
39
+ return state.parentOrigin;
40
+ }
41
+ state.parentOrigin = origin;
42
+ return state.parentOrigin;
43
+ }
44
+
45
+ export function getBoundPreviewParentOrigin(): string | null {
46
+ return getBridgeState().parentOrigin;
47
+ }
48
+
49
+ export function postToBoundPreviewParent(message: unknown): boolean {
50
+ if (typeof window === "undefined" || window.parent === window) return false;
51
+ const parentOrigin = getBoundPreviewParentOrigin();
52
+ if (!parentOrigin) return false;
53
+ window.parent.postMessage(message, parentOrigin);
54
+ return true;
55
+ }