create-next-imagicma 0.1.6 → 0.1.9
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 +6 -3
- package/bin/create-next-imagicma.mjs +19 -65
- package/package.json +1 -1
- package/template-hono/AGENTS.md +9 -1
- package/template-hono/README.md +18 -2
- package/template-hono/client/src/components/ui/button.tsx +3 -3
- package/template-hono/client/src/components/ui/checkbox.tsx +1 -1
- package/template-hono/client/src/lib/imagicma-preview-bridge.ts +55 -0
- package/template-hono/client/src/lib/imagicma-preview-picker.ts +1994 -0
- package/template-hono/client/src/lib/imagicma-preview-repair.ts +20 -34
- package/template-hono/client/src/main.tsx +3 -0
- package/template-hono/gitignore +1 -0
- package/template-hono/package.json +3 -2
- package/template-hono/pnpm-lock.yaml +46 -0
- package/template-hono/process-compose.yaml +0 -2
- package/template-hono/scripts/imagicma-common.mjs +56 -9
- package/template-hono/scripts/imagicma-dev.mjs +4 -3
- package/template-hono/scripts/imagicma-start.mjs +4 -1
- package/template-hono/server/app.ts +9 -0
- package/template-hono/server/index.ts +7 -11
- package/template-hono/vite.config.ts +34 -28
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,
|
|
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:
|
|
220
|
-
start:
|
|
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,
|
|
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(
|
|
399
|
+
console.log(`已写入运行时端口文件:.imagicma/runtime.env (PORT=${port})`);
|
|
446
400
|
}
|
|
447
401
|
if (theme !== undefined) {
|
|
448
402
|
console.log(`已设置默认主题:${theme}`);
|
package/package.json
CHANGED
package/template-hono/AGENTS.md
CHANGED
|
@@ -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
|
- 至少覆盖桌面与移动端关键断点,不允许内容溢出。
|
package/template-hono/README.md
CHANGED
|
@@ -26,7 +26,18 @@ pnpm install
|
|
|
26
26
|
pnpm dev
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
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
|
-
-
|
|
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
|
|
15
|
+
"bg-primary text-primary-foreground border border-primary/45",
|
|
16
16
|
destructive:
|
|
17
|
-
"bg-destructive text-destructive-foreground border border-destructive/45
|
|
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-[
|
|
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
|
+
}
|