create-zleap-app 0.0.0-stage → 0.10.1
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/index.js +160 -0
- package/lib/scaffold.js +184 -0
- package/package.json +39 -4
- package/skills/zleap-cloud-app/SKILL.md +130 -0
- package/skills/zleap-cloud-app/references/app-runtime.md +309 -0
- package/skills/zleap-cloud-app/references/cloud-functions.md +680 -0
- package/skills/zleap-cloud-app/references/console-database.md +428 -0
- package/skills/zleap-cloud-app/references/console-members.md +144 -0
- package/skills/zleap-cloud-app/references/limits.md +281 -0
- package/skills/zleap-cloud-app/references/manifest.md +723 -0
- package/skills/zleap-cloud-app/references/permissions.md +362 -0
- package/skills/zleap-cloud-app/references/sdk-database-files.md +705 -0
- package/skills/zleap-cloud-app/references/status-codes.md +648 -0
- package/skills/zleap-cloud-app/references/triggers.md +418 -0
- package/skills/zleap-cloud-app-release/SKILL.md +137 -0
- package/skills/zleap-cloud-app-release/references/app-center-install.md +361 -0
- package/skills/zleap-cloud-app-release/references/app-center-publish.md +257 -0
- package/skills/zleap-cloud-app-release/references/console-functions.md +557 -0
- package/skills/zleap-cloud-app-release/references/console-legacy-release.md +408 -0
- package/skills/zleap-cloud-app-release/references/console-members.md +144 -0
- package/skills/zleap-cloud-app-release/references/console-release.md +152 -0
- package/skills/zleap-cloud-app-release/references/console-workspace.md +254 -0
- package/skills/zleap-cloud-app-release/references/dev-to-production.md +339 -0
- package/skills/zleap-cloud-app-release/references/permissions.md +362 -0
- package/skills/zleap-cloud-app-release/references/status-codes.md +648 -0
- package/templates/react-ts/AGENTS.md +31 -0
- package/templates/react-ts/CLAUDE.md +1 -0
- package/templates/react-ts/LICENSE +21 -0
- package/templates/react-ts/README.md +48 -0
- package/templates/react-ts/_gitignore +8 -0
- package/templates/react-ts/index.html +12 -0
- package/templates/react-ts/manifest.json +33 -0
- package/templates/react-ts/package.json +33 -0
- package/templates/react-ts/scripts/zleap.mjs +365 -0
- package/templates/react-ts/src/App.tsx +237 -0
- package/templates/react-ts/src/cloud/client.ts +84 -0
- package/templates/react-ts/src/cloud/todos.ts +65 -0
- package/templates/react-ts/src/cloud/useCloudConnection.ts +25 -0
- package/templates/react-ts/src/index.css +241 -0
- package/templates/react-ts/src/main.tsx +11 -0
- package/templates/react-ts/src/vite-env.d.ts +1 -0
- package/templates/react-ts/tsconfig.app.json +23 -0
- package/templates/react-ts/tsconfig.json +4 -0
- package/templates/react-ts/tsconfig.node.json +19 -0
- package/templates/react-ts/vite.config.ts +66 -0
- package/README.md +0 -3
package/index.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-zleap-app:一条命令生成可直接部署到 zleapOS 的云开发应用。
|
|
3
|
+
//
|
|
4
|
+
// npm create zleap-app@latest my-app
|
|
5
|
+
// npx create-zleap-app my-app --appid zl3f9a2c7e1b0d4a68 --name "我的应用" -y
|
|
6
|
+
//
|
|
7
|
+
// appid 由平台生成:先在 zleapOS 云开发控制台「创建应用」,复制 appid。
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
11
|
+
import { createInterface } from 'node:readline/promises';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
TEMPLATES,
|
|
16
|
+
detectPackageManager,
|
|
17
|
+
installSkills,
|
|
18
|
+
isEmptyDir,
|
|
19
|
+
parseArgs,
|
|
20
|
+
scaffold,
|
|
21
|
+
toPackageName,
|
|
22
|
+
validateAppId,
|
|
23
|
+
validateName,
|
|
24
|
+
} from './lib/scaffold.js';
|
|
25
|
+
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'));
|
|
28
|
+
const kit = pkg.zleapKit;
|
|
29
|
+
|
|
30
|
+
const color = (code) => (text) => (process.stdout.isTTY ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
31
|
+
const bold = color('1');
|
|
32
|
+
const green = color('32');
|
|
33
|
+
const cyan = color('36');
|
|
34
|
+
const dim = color('2');
|
|
35
|
+
const red = color('31');
|
|
36
|
+
|
|
37
|
+
const HELP = `
|
|
38
|
+
${bold('create-zleap-app')} ${dim(`v${pkg.version}`)} — 创建 zleapOS 云开发应用
|
|
39
|
+
|
|
40
|
+
用法
|
|
41
|
+
npm create zleap-app@latest [目录] [-- 选项]
|
|
42
|
+
npx create-zleap-app [目录] [选项]
|
|
43
|
+
|
|
44
|
+
选项
|
|
45
|
+
--appid <appid> 应用 appid:在 zleapOS 云开发控制台「创建应用」后复制(形如 zl3f9a2c7e1b0d4a68)
|
|
46
|
+
--name <名称> 应用显示名称
|
|
47
|
+
--template <名> 模板:${TEMPLATES.join(' / ')}(默认 react-ts)
|
|
48
|
+
--target <URL> zleapOS 地址,写入项目配置,之后 npm run login 不再询问
|
|
49
|
+
--pm <名> 包管理器:npm / pnpm / yarn / bun(默认跟随调用方)
|
|
50
|
+
--no-install 不安装依赖
|
|
51
|
+
--no-skills 不在项目里安装 AI 编程工具 skills
|
|
52
|
+
--no-git 不初始化 git 仓库
|
|
53
|
+
-y, --yes 全部使用默认值,不提问
|
|
54
|
+
-h, --help 显示帮助
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
async function prompt(rl, question, fallback, validate) {
|
|
58
|
+
for (;;) {
|
|
59
|
+
const raw = (await rl.question(`${cyan('?')} ${question}${fallback ? dim(` (${fallback})`) : ''} `)).trim();
|
|
60
|
+
const value = raw || fallback;
|
|
61
|
+
const problem = validate ? validate(value) : null;
|
|
62
|
+
if (!problem) return value;
|
|
63
|
+
console.log(red(` ${problem}`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function run(command, args, cwd) {
|
|
68
|
+
const result = spawnSync(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
69
|
+
return result.status === 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
const options = parseArgs(process.argv.slice(2));
|
|
74
|
+
if (options.help) return console.log(HELP);
|
|
75
|
+
if (options.version) return console.log(pkg.version);
|
|
76
|
+
if (!TEMPLATES.includes(options.template)) throw new Error(`未知模板:${options.template}(可选 ${TEMPLATES.join(' / ')})`);
|
|
77
|
+
|
|
78
|
+
const interactive = process.stdin.isTTY && !options.yes;
|
|
79
|
+
const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
80
|
+
try {
|
|
81
|
+
let dir = options.dir;
|
|
82
|
+
if (!dir) dir = rl ? await prompt(rl, '项目目录', 'my-zleap-app') : 'my-zleap-app';
|
|
83
|
+
const targetDir = resolve(dir);
|
|
84
|
+
if (!isEmptyDir(targetDir)) throw new Error(`目录 ${dir} 不是空目录,为避免覆盖请换一个目录`);
|
|
85
|
+
|
|
86
|
+
let appid = options.appid;
|
|
87
|
+
if (!appid && rl) {
|
|
88
|
+
console.log(dim(' appid 在 zleapOS 云开发控制台「创建应用」后获得,复制过来粘贴即可。'));
|
|
89
|
+
appid = await prompt(rl, 'appid', '', validateAppId);
|
|
90
|
+
}
|
|
91
|
+
const idProblem = validateAppId(appid);
|
|
92
|
+
if (idProblem) throw new Error(`${idProblem}\n 用法:npm create zleap-app@latest ${dir} -- --appid <appid>`);
|
|
93
|
+
|
|
94
|
+
let name = options.name;
|
|
95
|
+
if (!name) name = rl ? await prompt(rl, '应用名称', toPackageName(dir), validateName) : toPackageName(dir);
|
|
96
|
+
const nameProblem = validateName(name);
|
|
97
|
+
if (nameProblem) throw new Error(nameProblem);
|
|
98
|
+
rl?.close();
|
|
99
|
+
|
|
100
|
+
console.log(`\n在 ${cyan(targetDir)} 创建 ${bold(name)}(${appid})`);
|
|
101
|
+
scaffold({
|
|
102
|
+
templateDir: join(here, 'templates', options.template),
|
|
103
|
+
targetDir,
|
|
104
|
+
appid,
|
|
105
|
+
name,
|
|
106
|
+
packageName: toPackageName(dir),
|
|
107
|
+
cliPackage: kit.cliPackage,
|
|
108
|
+
cliVersion: kit.cliVersion,
|
|
109
|
+
sdkPackage: kit.sdkPackage,
|
|
110
|
+
sdkVersion: kit.sdkVersion,
|
|
111
|
+
});
|
|
112
|
+
console.log(green('✔ 项目文件已生成'));
|
|
113
|
+
|
|
114
|
+
if (options.target) {
|
|
115
|
+
mkdirSync(join(targetDir, '.zleap'), { recursive: true });
|
|
116
|
+
writeFileSync(join(targetDir, '.zleap', 'config.json'), `${JSON.stringify({ target: options.target }, null, 2)}\n`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (options.skills) {
|
|
120
|
+
const names = installSkills({ skillsDir: join(here, 'skills'), targetDir });
|
|
121
|
+
if (names.length) console.log(green(`✔ 已为 AI 编程工具安装 skills:${names.join(', ')}(.agents/skills、.claude/skills)`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (options.git && !existsSync(join(targetDir, '.git'))) {
|
|
125
|
+
const inside = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: targetDir, encoding: 'utf8' });
|
|
126
|
+
if (inside.status !== 0 && run('git', ['init', '-q'], targetDir)) console.log(green('✔ 已初始化 git 仓库'));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const pm = options.pm || detectPackageManager();
|
|
130
|
+
let installed = false;
|
|
131
|
+
if (options.install) {
|
|
132
|
+
console.log(`\n使用 ${pm} 安装依赖…`);
|
|
133
|
+
installed = run(pm, ['install'], targetDir);
|
|
134
|
+
if (!installed) console.log(red(`依赖安装失败,可以稍后在项目目录手动运行 ${pm} install`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const cd = relative(process.cwd(), targetDir) || '.';
|
|
138
|
+
const runScript = (script) => (pm === 'npm' ? `npm run ${script}` : `${pm} ${script}`);
|
|
139
|
+
console.log(`
|
|
140
|
+
${bold('完成!接下来:')}
|
|
141
|
+
|
|
142
|
+
cd ${cd}${installed ? '' : `\n ${pm} install`}
|
|
143
|
+
${runScript('login')}${options.target ? '' : ' -- --target https://你的-os-地址'} ${dim('# 浏览器授权')}
|
|
144
|
+
${runScript('deploy')} ${dim('# 构建→检查→上传→装到你的桌面')}
|
|
145
|
+
|
|
146
|
+
然后从 zleapOS 启动台打开应用(开发环境与数据库在安装时自动开通)。
|
|
147
|
+
|
|
148
|
+
${bold('让 AI 编程工具帮你开发')}
|
|
149
|
+
项目里已带 AGENTS.md 和 skills,直接在 Codex / Claude Code / Pi / OpenCode / Cursor 里打开本目录即可。
|
|
150
|
+
想全局安装 skills:${cyan(`npx skills add ${kit.skillsRepo}`)}
|
|
151
|
+
`);
|
|
152
|
+
} finally {
|
|
153
|
+
rl?.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
main().catch((error) => {
|
|
158
|
+
console.error(red(`✖ ${error.message}`));
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
});
|
package/lib/scaffold.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// 项目生成的纯逻辑:参数解析、命名校验、模板复制与占位符替换、skills 安装。
|
|
2
|
+
// 不依赖第三方包,便于 npx 秒开,也便于单元测试。
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
/** 平台生成的 appid(云开发控制台「创建应用」得到),与服务端 pkg/appid.Platform 一致。 */
|
|
7
|
+
export const APPID_PATTERN = /^zl[0-9a-f]{16}$/;
|
|
8
|
+
|
|
9
|
+
/** 旧的命名空间 app_key(com.example.todo)。服务端在清理阶段之前仍接受,只为已有应用保留。 */
|
|
10
|
+
export const APP_KEY_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*)+$/;
|
|
11
|
+
|
|
12
|
+
export const TEMPLATES = ['react-ts'];
|
|
13
|
+
|
|
14
|
+
/** 需要做占位符替换的文本文件(相对模板根目录)。 */
|
|
15
|
+
const SUBSTITUTED = ['package.json', 'manifest.json', 'index.html', 'LICENSE', 'README.md', 'AGENTS.md'];
|
|
16
|
+
|
|
17
|
+
/** 为 AI 编程工具安装 skills 的项目内目录:.agents/skills 覆盖 Codex、Pi、OpenCode、Cursor、Gemini CLI、Copilot 等,.claude/skills 给 Claude Code。 */
|
|
18
|
+
export const SKILL_TARGETS = ['.agents/skills', '.claude/skills'];
|
|
19
|
+
|
|
20
|
+
/** 递归复制(不保留源权限位,跳过符号链接);skip(rel) 返回 true 的路径不复制。 */
|
|
21
|
+
export function copyTree(from, to, skip = () => false, base = from) {
|
|
22
|
+
mkdirSync(to, { recursive: true });
|
|
23
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
24
|
+
const source = join(from, entry.name);
|
|
25
|
+
if (skip(relative(base, source))) continue;
|
|
26
|
+
const target = join(to, entry.name);
|
|
27
|
+
if (entry.isDirectory()) copyTree(source, target, skip, base);
|
|
28
|
+
else if (entry.isFile()) copyFileSync(source, target);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** appid:优先平台 appid;旧命名空间 app_key 仍可用(已有应用)。 */
|
|
33
|
+
export function validateAppId(value) {
|
|
34
|
+
if (!value) return '请提供 appid:在 zleapOS 云开发控制台「创建应用」后复制(形如 zl3f9a2c7e1b0d4a68)';
|
|
35
|
+
if (APPID_PATTERN.test(value)) return null;
|
|
36
|
+
if (value.length <= 64 && APP_KEY_PATTERN.test(value)) return null;
|
|
37
|
+
return 'appid 格式不对:应是 zl 开头加 16 位小写十六进制(形如 zl3f9a2c7e1b0d4a68),在云开发控制台「创建应用」后复制';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateAppKey(value) {
|
|
41
|
+
if (!value) return '请提供 app_key,例如 com.yourteam.todo';
|
|
42
|
+
if (value.length > 64) return 'app_key 最长 64 个字符';
|
|
43
|
+
if (!APP_KEY_PATTERN.test(value)) {
|
|
44
|
+
return 'app_key 必须是带发布者前缀的小写命名空间:至少一个点,每段小写字母开头,只含小写字母、数字、- 或 _(例如 com.yourteam.todo)';
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validateName(value) {
|
|
50
|
+
if (!value || !value.trim()) return '应用名称不能为空';
|
|
51
|
+
if ([...value].length > 64) return '应用名称最多 64 个字符';
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 目录名 → npm 包名。 */
|
|
56
|
+
export function toPackageName(dir) {
|
|
57
|
+
const name = basename(resolve(dir))
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
60
|
+
.replace(/^[._-]+|[._-]+$/g, '');
|
|
61
|
+
return name || 'zleap-app';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 目录名 → app_key 最后一段。 */
|
|
65
|
+
export function toAppKeySegment(dir) {
|
|
66
|
+
const segment = basename(resolve(dir))
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
69
|
+
.replace(/^[^a-z]+/, '')
|
|
70
|
+
.replace(/-+$/, '');
|
|
71
|
+
return segment || 'app';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseArgs(argv) {
|
|
75
|
+
const out = { install: true, skills: true, git: true, yes: false, help: false, version: false, template: 'react-ts' };
|
|
76
|
+
const positional = [];
|
|
77
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
78
|
+
const arg = argv[i];
|
|
79
|
+
const [flag, inline] = arg.startsWith('--') ? arg.split(/=(.*)/s) : [arg];
|
|
80
|
+
const value = () => (inline !== undefined ? inline : argv[++i]);
|
|
81
|
+
switch (flag) {
|
|
82
|
+
case '--appid':
|
|
83
|
+
case '--app-id':
|
|
84
|
+
// --app-key 为旧参数名,等价于 --appid
|
|
85
|
+
case '--app-key':
|
|
86
|
+
out.appid = value();
|
|
87
|
+
break;
|
|
88
|
+
case '--name':
|
|
89
|
+
out.name = value();
|
|
90
|
+
break;
|
|
91
|
+
case '--template':
|
|
92
|
+
case '-t':
|
|
93
|
+
out.template = value();
|
|
94
|
+
break;
|
|
95
|
+
case '--pm':
|
|
96
|
+
out.pm = value();
|
|
97
|
+
break;
|
|
98
|
+
case '--target':
|
|
99
|
+
out.target = value();
|
|
100
|
+
break;
|
|
101
|
+
case '--no-install':
|
|
102
|
+
out.install = false;
|
|
103
|
+
break;
|
|
104
|
+
case '--no-skills':
|
|
105
|
+
out.skills = false;
|
|
106
|
+
break;
|
|
107
|
+
case '--no-git':
|
|
108
|
+
out.git = false;
|
|
109
|
+
break;
|
|
110
|
+
case '--yes':
|
|
111
|
+
case '-y':
|
|
112
|
+
out.yes = true;
|
|
113
|
+
break;
|
|
114
|
+
case '--help':
|
|
115
|
+
case '-h':
|
|
116
|
+
out.help = true;
|
|
117
|
+
break;
|
|
118
|
+
case '--version':
|
|
119
|
+
case '-v':
|
|
120
|
+
out.version = true;
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
if (arg.startsWith('-')) throw new Error(`未知参数:${arg}`);
|
|
124
|
+
positional.push(arg);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
out.dir = positional[0];
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function detectPackageManager(userAgent = process.env.npm_config_user_agent ?? '') {
|
|
132
|
+
for (const pm of ['pnpm', 'yarn', 'bun']) if (userAgent.startsWith(`${pm}/`)) return pm;
|
|
133
|
+
return 'npm';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function isEmptyDir(dir) {
|
|
137
|
+
if (!existsSync(dir)) return true;
|
|
138
|
+
if (!statSync(dir).isDirectory()) return false;
|
|
139
|
+
return readdirSync(dir).filter((name) => name !== '.git' && name !== '.DS_Store').length === 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const escapeHTML = (text) => text.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
143
|
+
|
|
144
|
+
/** 复制模板并替换占位符。返回写入的文件数。 */
|
|
145
|
+
export function scaffold({ templateDir, targetDir, appid, appKey, name, packageName, cliPackage, cliVersion, sdkPackage, sdkVersion = cliVersion, year = new Date().getFullYear() }) {
|
|
146
|
+
copyTree(templateDir, targetDir, (rel) => /(^|[\\/])(node_modules|dist|out|\.zleap)([\\/]|$)/.test(rel));
|
|
147
|
+
if (existsSync(join(targetDir, '_gitignore'))) renameSync(join(targetDir, '_gitignore'), join(targetDir, '.gitignore'));
|
|
148
|
+
|
|
149
|
+
const replacements = {
|
|
150
|
+
__APPID__: appid ?? appKey,
|
|
151
|
+
__APP_NAME__: name,
|
|
152
|
+
__PKG_NAME__: packageName,
|
|
153
|
+
__CLI_PACKAGE__: cliPackage,
|
|
154
|
+
__CLI_VERSION__: cliVersion,
|
|
155
|
+
__SDK_PACKAGE__: sdkPackage,
|
|
156
|
+
__SDK_VERSION__: sdkVersion,
|
|
157
|
+
__YEAR__: String(year),
|
|
158
|
+
};
|
|
159
|
+
for (const rel of SUBSTITUTED) {
|
|
160
|
+
const file = join(targetDir, rel);
|
|
161
|
+
if (!existsSync(file)) continue;
|
|
162
|
+
let text = readFileSync(file, 'utf8');
|
|
163
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
164
|
+
// JSON 文件里的值需要按 JSON 字符串转义(应用名可能包含引号)
|
|
165
|
+
const safe = rel.endsWith('.json') ? JSON.stringify(value).slice(1, -1) : rel.endsWith('.html') ? escapeHTML(value) : value;
|
|
166
|
+
text = text.split(key).join(safe);
|
|
167
|
+
}
|
|
168
|
+
writeFileSync(file, text);
|
|
169
|
+
}
|
|
170
|
+
const leftover = SUBSTITUTED.filter((rel) => existsSync(join(targetDir, rel)) && /__[A-Z_]+__/.test(readFileSync(join(targetDir, rel), 'utf8')));
|
|
171
|
+
if (leftover.length) throw new Error(`模板占位符未替换完整:${leftover.join(', ')}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 把随包分发的 skills 复制到项目内各 AI 工具的 skills 目录。返回安装的 skill 名称。 */
|
|
175
|
+
export function installSkills({ skillsDir, targetDir }) {
|
|
176
|
+
if (!existsSync(skillsDir)) return [];
|
|
177
|
+
const names = readdirSync(skillsDir).filter((name) => existsSync(join(skillsDir, name, 'SKILL.md')));
|
|
178
|
+
for (const target of SKILL_TARGETS) {
|
|
179
|
+
for (const name of names) {
|
|
180
|
+
copyTree(join(skillsDir, name), join(targetDir, target, name));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return names;
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-zleap-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.10.1",
|
|
4
|
+
"description": "一条命令创建 zleapOS 云开发应用(React + TypeScript + Vite),并为 Codex / Claude Code / Pi / OpenCode / Cursor 等 AI 编程工具配好 skills",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/zleap-ai/zleap-app-kit.git",
|
|
9
|
+
"directory": "packages/create-zleap-app"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"create-zleap-app": "index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"lib",
|
|
18
|
+
"templates",
|
|
19
|
+
"skills"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.17"
|
|
23
|
+
},
|
|
24
|
+
"zleapKit": {
|
|
25
|
+
"cliPackage": "@zleap-ai/app-cli",
|
|
26
|
+
"cliVersion": "0.10.0",
|
|
27
|
+
"sdkPackage": "@zleap-ai/app-sdk",
|
|
28
|
+
"sdkVersion": "0.10.0",
|
|
29
|
+
"skillsRepo": "zleap-ai/zleap-app-kit"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"zleap",
|
|
33
|
+
"zleapos",
|
|
34
|
+
"app-cloud",
|
|
35
|
+
"create",
|
|
36
|
+
"scaffold",
|
|
37
|
+
"react",
|
|
38
|
+
"vite",
|
|
39
|
+
"agent-skills"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zleap-cloud-app
|
|
3
|
+
description: Build, modify, debug and deploy zleapOS 云开发 (App Cloud) apps — manifest.json with "schema":"zleap.app/2", Cloud SDK (zl.cloud database / files / callFunction), sandboxed iframe UI, and the zleap-app CLI (check / pack / publish / install / diagnose). Use when a project contains a zleap.app/2 manifest or src/zleap-sdk, imports zl from the zleap SDK, or the user mentions zleapOS, 云开发, App Cloud, 应用中心, 云开发控制台, create-zleap-app or zleap-app, or asks to create a new zleapOS app.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# zleapOS 云开发应用:开发、调试、部署到自己桌面
|
|
7
|
+
|
|
8
|
+
zleapOS 云开发应用 = 一个网页(运行在 zleapOS 桌面的沙箱 iframe 里)+ 一份 `manifest.json`。
|
|
9
|
+
数据库、文件、后台任务都由平台代管,前端通过 Cloud SDK 调用。上线到生产、给同事用、云函数签名激活,见 skill `zleap-cloud-app-release`。
|
|
10
|
+
|
|
11
|
+
## 0. 先判断项目形态
|
|
12
|
+
|
|
13
|
+
| 看到 | 说明 | 构建/打包目录 |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `vite.config.ts` + `src/cloud/` + `scripts/zleap.mjs` | create-zleap-app 的 React 模板(推荐) | `dist/`(构建产物) |
|
|
16
|
+
| 根目录 `index.html` + `main.js` + `sdk/` | `zleap-app init hosted-package` 的原生模板 | 项目根目录 |
|
|
17
|
+
| 都没有 | 新建项目,见第 1 节 | — |
|
|
18
|
+
|
|
19
|
+
两种形态的 SDK 是同一套 API,只是来源不同:React 模板从 npm 包 `@zleap-ai/app-sdk` 导入(`/host` 宿主能力、`/workspace` 工作区),
|
|
20
|
+
以 `node_modules/@zleap-ai/app-sdk/dist/types/*.d.ts` 为准,并与 `@zleap-ai/app-cli` 保持同版本;原生模板用项目里的 `./sdk/*.js`。
|
|
21
|
+
参考资料里写 `./sdk/index.js` / `./sdk/zleap-sdk.js` 的地方,在 React 模板里分别对应 `@zleap-ai/app-sdk` / `@zleap-ai/app-sdk/host`。
|
|
22
|
+
|
|
23
|
+
## 1. 新建项目(非交互,适合 AI 执行)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm create zleap-app@latest <目录> -- --appid <appid> --name "<应用名>" -y
|
|
27
|
+
# 已知 OS 地址时加:--target https://os.example.com
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`appid` 由平台生成,**不能自己编**:请用户先在云开发控制台侧栏顶部的应用切换下拉里点「创建应用」(填名称),复制生成的 `appid`
|
|
31
|
+
(`zl` + 16 位小写十六进制,如 `zl3f9a2c7e1b0d4a68`),创建者自动成为管理员。没有 `appid` 时非交互脚手架会报错并提示先创建应用——问用户要,不要猜。
|
|
32
|
+
manifest 里写 `"appid": "<appid>"`,**不需要** `app_key` 和 `publisher`。旧项目的 `app_key`(如 `com.example.todo`)只为已有应用保留;`--app-key` 是 `--appid` 的旧别名。
|
|
33
|
+
同事要一起开发,请管理员在控制台「团队」→「成员」里把他加为开发者。每个成员的每次上传都是一个开发版本,在控制台「版本管理」里可见;
|
|
34
|
+
成员可以用「我也用这个」运行同事上传、未发布的版本(非成员看不到开发版本)。给全企业用 = 在「版本管理」里「发布为线上版本」,见 skill `zleap-cloud-app-release`。
|
|
35
|
+
|
|
36
|
+
## 2. 硬性约束(违反就会在运行时失败)
|
|
37
|
+
|
|
38
|
+
1. **一切先在 manifest 声明**:集合 `data.collections` + 结构 `data.schemas`;权限 `runtime.capabilities.requires`。没声明的拿不到。
|
|
39
|
+
2. **集合结构上线即固定**:已建集合不随新版本更新 Schema;`additionalProperties` 缺省按 `false`;集合键 `^[a-z][a-z0-9_]{0,63}$`,不能用 `sys_`/`ac_`/`t_` 前缀。要改字段 → 新集合 + 迁移。动手改 schema 前先告诉用户这一点。
|
|
40
|
+
3. **沙箱**:opaque origin,CSP `connect-src 'none'`、`worker-src 'none'`、脚本/样式/字体只能来自包内。所以:不能 `fetch`/XHR/WebSocket、不能用 CDN 或在线字体、不能用 `localStorage`/`sessionStorage`/`indexedDB`、不能 `alert/confirm/prompt`、不能开 Worker、不能弹新窗口。状态放内存或云数据库。
|
|
41
|
+
4. **环境由宿主决定**:`zl.cloud.init({ env: zl.cloud.DYNAMIC_CURRENT_ENV })`,绝不写死 `'dev'`/`'prod'`。
|
|
42
|
+
5. **读取上限**:单次 `limit` ≤ 100,`skip` ≤ 1000,用分页;查询有筛选/排序但没索引时可能 `INDEX_REQUIRED`。
|
|
43
|
+
6. **乐观锁**:`update`/`remove` 带 `agentos: { expectedVersion: doc._version }`;冲突 `VERSION_CONFLICT` → 重读再改。
|
|
44
|
+
7. **结果未知不重试**:`COMMIT_UNKNOWN`(CLI 退出码 6)→ 先重读或查询原操作,绝不自动重复提交。
|
|
45
|
+
8. **明确失败优于假成功**:不要用 mock 数据、假登录、吞错误让界面「看起来能用」。浏览器直接打开(`npm run dev`)时 SDK 连不上宿主是正常的。
|
|
46
|
+
9. **凭据**:登录只能由用户在浏览器确认(`npm run login`),不要索要或输入密码,不要读取/打印 `~/.config/zleap-app/session.json`。
|
|
47
|
+
|
|
48
|
+
## 3. 开发一个功能的标准流程
|
|
49
|
+
|
|
50
|
+
1. **建模**:在 `manifest.json` 加集合与 strict schema(类型只支持 object/array/string/number/integer/boolean/null,关键字子集见 references/manifest.md)。日期用 `number` 存毫秒,或 `integer` + `"x-agentos-value-type": "date_ms"`。
|
|
51
|
+
2. **权限**:需要的能力加进 `requires`(`cloud.database.read/write`、`cloud.storage.read/write`、`os.agent.complete` 等,白名单外的非可选能力会让应用「暂不可用」)。`reason` 会展示给用户,写具体用途。
|
|
52
|
+
3. **数据层**:React 模板在 `src/cloud/` 下按集合写一个模块(参照 `src/cloud/todos.ts`),组件只调用这些函数;错误用 `describeError()` 展示。
|
|
53
|
+
4. **本地验证**:`npm run typecheck && npm run check`(`check` 通过只代表静态契约,`runtime_test=not_checked`)。
|
|
54
|
+
5. **部署到自己桌面**:`npm run deploy`(构建 → 检查 → 上传 private → 安装/切换本人安装;内容变化时自动递增补丁号)。
|
|
55
|
+
- 报未登录/会话过期 → 请用户运行 `npm run login -- --target <OS 地址>` 并在浏览器核对确认码。
|
|
56
|
+
- 报 `DATA_MIGRATION_REQUIRED` → 新版本改了 data/storage 声明,见第 5 节。
|
|
57
|
+
6. **请用户验收**:安装时平台会自动创建开发环境、开通数据库并按声明建集合,`npm run deploy` 结束时会打印状态;显示未就绪时,请用户在「云开发控制台」选中应用 →「概览」按提示处理。然后从启动台打开应用(已打开的点「重新加载应用」)。
|
|
58
|
+
7. **排查**:`npm run status`(安装、权限、版本)、`npm run logs`(24h 页面结构化错误 + 本人云函数日志,自动选当前所在环境)、控制台「运行体检」。
|
|
59
|
+
|
|
60
|
+
`npx zleap-app …` 只在装了 `@zleap-ai/app-cli` 的项目里可用;项目外或原生模板(没有 package.json)请用 `npx @zleap-ai/app-cli …`。原生模板没有 npm 脚本时,用 CLI 等价命令:`npx @zleap-ai/app-cli check .`、`publish .`、`install <app_id>`、`diagnose <app_id>`、`update <app_id> --expected-version-id <当前> --version-id <新>`。
|
|
61
|
+
|
|
62
|
+
### 版本更新提示与授权登录(不需要云资源)
|
|
63
|
+
|
|
64
|
+
- **提示新版本**:`zl.app.getUpdateManager()` → `onCheckForUpdate` / `onUpdateReady` / `onUpdateFailed` / `applyUpdate()`(切到线上版本并重新加载)。
|
|
65
|
+
非成员下次打开时宿主会自动跟随线上版本,这里只是让开着的窗口也能及时更新。提示 UI 要自己画(沙箱里不能用 `confirm`)。
|
|
66
|
+
- **拿用户信息**:先在 manifest `host_api.requires` 声明 `user.profile` / `user.phone` / `user.email`(没声明 `AUTH_SCOPE_UNDECLARED`),
|
|
67
|
+
再在**按钮点击事件里**调用 `zl.auth.authorize({ scopes: ['profile' | 'phone' | 'email'], reason: '用途说明' })`(不在点击里调用 `AUTH_GESTURE_REQUIRED`),
|
|
68
|
+
返回 `{ openid, scopes, profile?, phone?, email? }`——用户可以只勾选一部分、没绑定的不返回,**以 `scopes` 判断拿到了什么**;
|
|
69
|
+
拒绝时 `AUTH_DENIED`(拒绝过的项 24 小时内不再弹框,直接 `AUTH_DENIED`)。`zl.auth.getIdentity()` 静默拿 `openid`(每个应用不同、开发与线上一致)。
|
|
70
|
+
云函数里用 `zl.cloud.auth.getOpenId()` / `getUserInfo()`;不要用 `getContext().userId`(平台内部 ID,已废弃)。
|
|
71
|
+
不要自己做登录页或索要账号;云函数里目前读不到授权信息。
|
|
72
|
+
- 都从 `@zleap-ai/app-sdk` 导入(`import { zl, AppError } from '@zleap-ai/app-sdk'`),完整说明见 references/app-runtime.md。
|
|
73
|
+
|
|
74
|
+
## 4. 只有用户能做的事(明确告诉用户,不要假装已完成)
|
|
75
|
+
|
|
76
|
+
| 事项 | 在哪里 | 真实文案 |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| 浏览器授权登录 | `npm run login` 打开的网页 | 核对确认码后允许 |
|
|
79
|
+
| 开发环境未自动创建时 | 云开发控制台 → 概览 | 「创建开发环境」 |
|
|
80
|
+
| 创建应用拿 `appid` | 云开发控制台 → 侧栏顶部应用切换下拉 | 「创建应用」 |
|
|
81
|
+
| 添加协作开发者 | 云开发控制台 →「团队」→「成员」(管理员) | 「添加成员」,输入登录名、手机号或邮箱 |
|
|
82
|
+
| 调整数据/文件/后台权限 | 应用窗口顶部 | 「权限」→「应用权限」对话框 →「保存并重新加载」(KV / 空间 / 通知在底部「管理其他权限」) |
|
|
83
|
+
| 看数据、建索引、备份 | 云开发控制台 → 数据库 | 「同步 Manifest 中的集合」、索引页 |
|
|
84
|
+
| 签名、激活、上生产、分享 | 见 skill `zleap-cloud-app-release` | — |
|
|
85
|
+
| 发布为线上版本 / 回退(appid 应用) | 云开发控制台 →「发布」→「版本管理」 | 「发布为线上版本」/「回退到此版本」 |
|
|
86
|
+
| 用户授权头像昵称 / 手机号 / 邮箱 | 应用里调用 `zl.auth.authorize` 后的系统授权框 | 「允许」;撤销在窗口顶部「已授权信息」 |
|
|
87
|
+
|
|
88
|
+
## 5. 常见问题速查
|
|
89
|
+
|
|
90
|
+
**先读错误本身。** CLI 出错时打印 `平台错误 <code>: <message> detail=<JSON>`:`message` 是中文说明(数据类错误会带具体上限、已用量),
|
|
91
|
+
`detail` 里的 `reason` 是稳定错误码,`next_action` 是平台给的下一步——有 `next_action` 就照它做,不要自己猜。
|
|
92
|
+
应用(iframe)里只拿得到 `errCode`,看不到这些说明;要看原文去控制台日志或用 CLI 复现。CLI 退出码:6 = 结果未知,2 = 其他错误。
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
| 现象 / 错误码 | 原因与处理 |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| 连接超时 `CALL_TIMEOUT`、独立预览 | 不在 zleapOS 窗口里运行;部署后从启动台打开 |
|
|
98
|
+
| `RESOURCE_NOT_READY` / SDK 没协商到 `document-core/1` | 当前数据域没有环境或数据库未就绪 → 等 1 分钟后重开应用;仍不行请用户在控制台概览检查开发环境与数据库 |
|
|
99
|
+
| `SCOPE_FORBIDDEN` | 没授权(服务端原因 `CONSENT_REQUIRED` 等)→ 请用户点窗口顶部「权限」勾选保存;或 `requires` 没声明 |
|
|
100
|
+
| `ENV_FORBIDDEN` | 部署已开放生产、应用还没有生产环境,非发布者打不开 → 发布者激活到生产(见 release skill) |
|
|
101
|
+
| `COLLECTION_NOT_FOUND`(上生产后) | 生产环境建了但没开通集合 → 控制台生产环境「数据库」开通,或 `npx zleap-app prepare` |
|
|
102
|
+
| `PUBLISHER_REQUIRED` | 非发布者访问开发数据域(默认只给发布者) |
|
|
103
|
+
| `SCHEMA_INVALID` | 写入字段与集合结构不符(缺必填、类型错、多了字段) |
|
|
104
|
+
| `VERSION_CONFLICT` | 别处先改过,重读后再改 |
|
|
105
|
+
| `INDEX_REQUIRED` | 给筛选/排序字段建索引(必填的数字/日期,或 maxLength ≤ 191 的字符串;每集合 ≤ 4 个);已有索引仍超限 → 缩小筛选范围 |
|
|
106
|
+
| `APP_RATE_LIMITED` | 本应用并发请求过多(默认同时 8 个)→ 稍后重试,别循环重试 |
|
|
107
|
+
| 应用中心显示「暂不可用」 | 非可选 requires 超出白名单、`host_api` 写了白名单外的值、声明了 `agent.interaction` 等 |
|
|
108
|
+
| 上传「该版本已经存在,请更换版本号」 | 同版本号不同内容;改 `version`(`npm run deploy` 会自动处理) |
|
|
109
|
+
| `DATA_MIGRATION_REQUIRED` | 改了 data/storage 声明,CLI 拒绝切换。开发期可 `npx zleap-app uninstall <app_id>` 后重新 `npm run deploy`(卸载保留数据);已建集合结构仍不会变 |
|
|
110
|
+
| 上传 40400「appid 不存在」(`APP_NOT_FOUND`) | `appid` 没在控制台创建或抄错 → 请用户在云开发控制台「创建应用」后复制 |
|
|
111
|
+
| 上传 40300「你不是这个应用的开发者」(`NOT_APP_MEMBER`) | 当前登录账号不是应用成员 → 请应用管理员在控制台「成员」里把他加为开发者 |
|
|
112
|
+
| 「manifest 的 appid 与 app_key 不一致」 | 两个字段都写了且不同 → 只保留 `appid` |
|
|
113
|
+
|
|
114
|
+
完整错误码:references/status-codes.md。
|
|
115
|
+
|
|
116
|
+
## 6. 参考资料(按需读取,别一次全读)
|
|
117
|
+
|
|
118
|
+
- references/manifest.md — Manifest v2 全字段、集合 Schema 子集、分区与记录规则、打包规则
|
|
119
|
+
- references/sdk-database-files.md — Cloud SDK:初始化与 profile、查询/写入/watch/onChange、文件上传下载、操作编号、COMMIT_UNKNOWN
|
|
120
|
+
- references/cloud-functions.md — 云函数、headless 能力、`os.agent.complete`、`capabilities.invoke` / `jobs.wait`
|
|
121
|
+
- references/triggers.md — 定时 / 事件触发器
|
|
122
|
+
- references/permissions.md — 安装授权、应用窗口「权限」(Cloud 数据授权)、数据域、后台能力授权
|
|
123
|
+
- references/console-database.md — 控制台数据库页(索引、备份恢复、筛选)
|
|
124
|
+
- references/console-members.md — 控制台「创建应用」拿 appid、成员与角色、上传时的成员校验
|
|
125
|
+
- references/app-runtime.md — `zl.app.getUpdateManager()` 版本更新、`zl.auth.authorize()` 授权登录、openid、AppError 错误码
|
|
126
|
+
- references/limits.md — 数值上限与平台未开放的能力
|
|
127
|
+
- references/status-codes.md — 状态与错误码
|
|
128
|
+
|
|
129
|
+
可选:支持 MCP 的工具可以接入同一 CLI 的 MCP 服务(工具 `app_spec_get`、`app_check`、`app_publish`、`app_diagnose` 等):
|
|
130
|
+
`npx -y @zleap-ai/app-cli mcp serve --workspace <项目绝对路径> --target <OS 地址>`(需先在终端 `auth login`)。
|