@playcraft/cli 0.0.56 → 0.0.57

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
@@ -93,23 +93,20 @@ playcraft remix start \
93
93
 
94
94
  ### 6. 将 Remix 源码 clone 到本地
95
95
 
96
+ 完整步骤(登录 → clone → commit → push)见:
97
+
98
+ - [Remix 本地 Clone / Commit / Push](../../docs/cli/remix-local-clone-push.md)
99
+
96
100
  ```bash
97
101
  playcraft login
98
- # 本地开发建议直连 backend,避免经前端代理缓冲 Git pack
99
- export PLAYCRAFT_API_URL=http://localhost:3001
100
-
101
102
  # 推荐:粘贴 Remix 详情页 / Editor / 工蜂分支页 URL
102
103
  playcraft remix clone '<remix-detail-or-editor-or-gongfeng-url>' --install
103
104
 
104
- # 兼容:显式 project + branch
105
- playcraft remix clone \
106
- --project-id <project-id> \
107
- --branch remix/your-branch \
108
- --install
109
-
110
105
  cd <cloned-dir>
111
106
  npm run dev # external 引擎
112
- git push # 经 PlayCraft Git 代理写回 Remix 分支(禁止推 main)
107
+ git add -A && git commit -m "…"
108
+ playcraft remix pull # 需要同步远端时
109
+ playcraft remix push # 不要裸 git push/pull(尤其 Windows)
113
110
  ```
114
111
 
115
112
  完整的 Remix 子命令、参数、图片传输、自动化输出及故障处理说明见:
package/REMIX.md CHANGED
@@ -24,6 +24,8 @@ playcraft remix start
24
24
  playcraft remix prompt
25
25
  playcraft remix run
26
26
  playcraft remix clone
27
+ playcraft remix pull
28
+ playcraft remix push
27
29
  playcraft git-credential
28
30
  ```
29
31
 
@@ -36,6 +38,8 @@ playcraft git-credential
36
38
  | 向已有 Remix 发送需求 | `playcraft remix prompt` |
37
39
  | 创建 Remix 并立即运行 Agent | `playcraft remix run` |
38
40
  | **Clone Remix 源码到本地** | `playcraft remix clone <详情页/Editor/工蜂 URL>` 或 `--project-id` + `--branch` |
41
+ | **Pull 远端 Remix** | `playcraft remix pull`(推荐;Windows 勿直接 `git pull`) |
42
+ | **Push 回 Remix 分支** | `playcraft remix push`(推荐;Windows 勿直接 `git push`) |
39
43
  | 查询已有 Remix 列表 | `playcraft tools list-remixes`;当前没有 `playcraft remix list` |
40
44
  | 给本地项目应用引擎模板 | `playcraft remix init-template` |
41
45
 
@@ -725,6 +729,8 @@ playcraft remix start --project-id <project-id> --branch <remix-branch> --open
725
729
 
726
730
  登录后可将 Remix 源码经 **PlayCraft Backend Git Smart HTTP 代理**克隆到本机,无需工蜂凭据。平台服务账号 Token 不会下发到 CLI。
727
731
 
732
+ > **上手文档(推荐先读)**:[Remix 本地 Clone / Commit / Push](../../docs/cli/remix-local-clone-push.md)
733
+
728
734
  ### 14.1 Clone
729
735
 
730
736
  ```bash
@@ -772,17 +778,23 @@ playcraft remix clone \
772
778
  - **external(Phaser / Three.js)**:`npm install && npm run dev`(可用 `--install` / `--dev`)
773
779
  - **PlayCanvas**:使用 `playcraft start --project <id>`,不是 Vite
774
780
 
775
- ### 14.3 Push 回 Remix
781
+ ### 14.3 Pull / Push
776
782
 
777
783
  在克隆目录内:
778
784
 
779
785
  ```bash
786
+ # 同步远端(例如沙箱 Save 之后)
787
+ playcraft remix pull
788
+
789
+ # 本地改完并 commit 后写回
780
790
  git add -A
781
791
  git commit -m "local changes"
782
- git push
792
+ playcraft remix push
783
793
  ```
784
794
 
785
- 服务端强制:
795
+ **不要**依赖裸 `git pull` / `git push`(尤其 Windows + Git Credential Manager 会弹窗)。二者与 clone 一样注入 `Authorization: Bearer`,并禁用本命令的 credential helper。
796
+
797
+ 服务端强制(push):
786
798
 
787
799
  - 只能 push 到 URL 对应的 Remix 分支
788
800
  - 禁止 push `main` / `master` / 项目默认分支
@@ -791,11 +803,11 @@ git push
791
803
 
792
804
  ### 14.4 与云端沙箱同时写
793
805
 
794
- 同一 Remix 分支若已在沙箱打开并 Save,本机 `git push` 与沙箱 push 会互相覆盖。请避免两边同时写同一分支;push 成功后 backend 会 `BareRepoCache.invalidate`。
806
+ 同一 Remix 分支若已在沙箱打开并 Save,本机 `playcraft remix push` 与沙箱 push 会互相覆盖。请避免两边同时写同一分支;需要合并远端时先 `playcraft remix pull`。push 成功后 backend 会 `BareRepoCache.invalidate`。
795
807
 
796
808
  ### 14.5 `playcraft git-credential`
797
809
 
798
- 供本机 `git pull` / `git push` 使用的 credential helper,从 `~/.playcraft/config.json` 读取 PAT。clone 后会自动写入仓库 `credential.helper`。
810
+ clone 后仍可能写入仓库 `credential.helper`(兼容)。**日常请用 `playcraft remix pull` / `push`**,不要手动跑 `git-credential`。
799
811
 
800
812
  ---
801
813
 
@@ -6,6 +6,8 @@ import { PlayCraftApiError, PlayCraftHttpClient, isPlayCraftApiError } from '../
6
6
  import { IMAGE_OPTION_HELP, buildReferenceImagesPrompt, readImageAttachments, uploadImageAttachments, } from '../utils/remix-image-attachments.js';
7
7
  import { initTemplateCommand } from '../remix/init-template.js';
8
8
  import { remixClone, gitCredentialCommand, isRemoteCloneSource } from '../remix/clone.js';
9
+ import { remixPush } from '../remix/push.js';
10
+ import { remixPull } from '../remix/pull.js';
9
11
  // Register autocomplete prompt
10
12
  inquirer.registerPrompt('autocomplete', autocomplete);
11
13
  const JSON_OUTPUT_HELP = '输出一条 JSON (JavaScript Object Notation, JavaScript 对象表示法) 结果';
@@ -1303,12 +1305,56 @@ export function registerRemixCommands(program) {
1303
1305
  handleCommandError('remix.clone', opts, undefined, error);
1304
1306
  }
1305
1307
  });
1308
+ remix
1309
+ .command('push')
1310
+ .description('经 PlayCraft Git 代理推送当前 Remix(注入已登录 PAT,避免 Windows Git Credential Manager 弹窗)')
1311
+ .argument('[git-args...]', '透传给 git push 的参数(可选)')
1312
+ .option('--dir <path>', '仓库目录(默认当前目录)')
1313
+ .option('--url <url>', SERVICE_ADDRESS_HELP)
1314
+ .option('--token <token>', PAT_HELP)
1315
+ .option('--json', JSON_OUTPUT_HELP)
1316
+ .action(async (gitArgs, opts) => {
1317
+ try {
1318
+ await remixPush({
1319
+ dir: opts.dir ? String(opts.dir) : undefined,
1320
+ url: opts.url,
1321
+ token: opts.token,
1322
+ json: Boolean(opts.json),
1323
+ gitArgs: Array.isArray(gitArgs) ? gitArgs.map(String) : [],
1324
+ });
1325
+ }
1326
+ catch (error) {
1327
+ handleCommandError('remix.push', opts, undefined, error);
1328
+ }
1329
+ });
1330
+ remix
1331
+ .command('pull')
1332
+ .description('经 PlayCraft Git 代理拉取当前 Remix(注入已登录 PAT,避免 Windows Git Credential Manager 弹窗)')
1333
+ .argument('[git-args...]', '透传给 git pull 的参数(可选)')
1334
+ .option('--dir <path>', '仓库目录(默认当前目录)')
1335
+ .option('--url <url>', SERVICE_ADDRESS_HELP)
1336
+ .option('--token <token>', PAT_HELP)
1337
+ .option('--json', JSON_OUTPUT_HELP)
1338
+ .action(async (gitArgs, opts) => {
1339
+ try {
1340
+ await remixPull({
1341
+ dir: opts.dir ? String(opts.dir) : undefined,
1342
+ url: opts.url,
1343
+ token: opts.token,
1344
+ json: Boolean(opts.json),
1345
+ gitArgs: Array.isArray(gitArgs) ? gitArgs.map(String) : [],
1346
+ });
1347
+ }
1348
+ catch (error) {
1349
+ handleCommandError('remix.pull', opts, undefined, error);
1350
+ }
1351
+ });
1306
1352
  }
1307
- /** Register `playcraft git-credential` for local git pull/push after remix clone. */
1353
+ /** Register `playcraft git-credential`(clone 写入的 helper;拉/推请用 remix pull / push). */
1308
1354
  export function registerGitCredentialCommand(program) {
1309
1355
  program
1310
1356
  .command('git-credential')
1311
- .description('Git credential helper:从登录配置提供 PlayCraft PAT')
1357
+ .description('Git credential helper(一般勿手动调用;请用 playcraft remix pull / push)')
1312
1358
  .argument('[action]', 'get | store | erase', 'get')
1313
1359
  .action((action) => {
1314
1360
  gitCredentialCommand(action);
@@ -200,7 +200,8 @@ export async function remixClone(options) {
200
200
  else {
201
201
  console.log(' playcraft start --project <project-id> # PlayCanvas / 本地 Agent 预览');
202
202
  }
203
- console.log(' git pull / git push # 经 PlayCraft Git 代理(需已 login)');
203
+ console.log(' playcraft remix pull # 拉取远端(沙箱 Save 后)');
204
+ console.log(' playcraft remix push # 推回 Remix(Windows 勿直接 git push/pull)');
204
205
  }
205
206
  export { isRemoteCloneSource, resolveRemixCloneTarget };
206
207
  /**
@@ -0,0 +1,56 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ /** @internal exported for tests */
5
+ export function readRemixLocalMeta(repoDir) {
6
+ const metaPath = path.join(repoDir, '.playcraft.json');
7
+ if (!existsSync(metaPath)) {
8
+ throw new Error(`未找到 .playcraft.json(当前目录:${repoDir})。请先在该目录执行 playcraft remix clone,或 cd 到 clone 得到的目录。`);
9
+ }
10
+ try {
11
+ const raw = JSON.parse(readFileSync(metaPath, 'utf8'));
12
+ const projectId = String(raw.projectId || '').trim();
13
+ const branch = String(raw.branch || '').trim();
14
+ const cloneUrl = String(raw.cloneUrl || '').trim();
15
+ if (!projectId || !branch || !cloneUrl) {
16
+ throw new Error('invalid_meta');
17
+ }
18
+ return {
19
+ schemaVersion: '1.0',
20
+ projectId,
21
+ branch,
22
+ cloneUrl,
23
+ clonedAt: typeof raw.clonedAt === 'string' ? raw.clonedAt : new Date().toISOString(),
24
+ };
25
+ }
26
+ catch (error) {
27
+ if (error instanceof Error && error.message.includes('.playcraft.json'))
28
+ throw error;
29
+ throw new Error(`.playcraft.json 无效或损坏:${metaPath}`);
30
+ }
31
+ }
32
+ /** @internal exported for tests */
33
+ export function assertPlaycraftProxyRemote(cloneUrl) {
34
+ if (!cloneUrl.includes('/api/git/projects/')) {
35
+ throw new Error(`当前 .playcraft.json 的 cloneUrl 不是 PlayCraft Git 代理地址。\n` +
36
+ `请用 playcraft remix clone 重新克隆,不要改 remote 到工蜂 / 其它 Git 主机。\n` +
37
+ `cloneUrl: ${cloneUrl}`);
38
+ }
39
+ }
40
+ /**
41
+ * 运行带 PlayCraft PAT 的 git 子命令:注入 Bearer,并清空 credential.helper,
42
+ * 避免 Windows Git Credential Manager 弹窗。
43
+ */
44
+ export function runAuthenticatedGit(gitCommandAndArgs, options) {
45
+ return spawnSync('git', [
46
+ '-c',
47
+ 'credential.helper=',
48
+ '-c',
49
+ `http.extraHeader=Authorization: Bearer ${options.token}`,
50
+ ...gitCommandAndArgs,
51
+ ], {
52
+ cwd: options.cwd,
53
+ encoding: 'utf8',
54
+ stdio: options.pipe ? 'pipe' : 'inherit',
55
+ });
56
+ }
@@ -0,0 +1,64 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { PlayCraftHttpClient } from '../utils/playcraft-http-client.js';
5
+ import { assertPlaycraftProxyRemote, readRemixLocalMeta, runAuthenticatedGit, } from './local-git.js';
6
+ /**
7
+ * 从 PlayCraft Git 代理拉取当前 Remix。
8
+ * 与 remix push 相同:注入 Bearer PAT,并禁用 credential.helper(避免 Windows GCM)。
9
+ */
10
+ export async function remixPull(options = {}) {
11
+ const repoDir = path.resolve(options.dir?.trim() || process.cwd());
12
+ if (!existsSync(path.join(repoDir, '.git'))) {
13
+ throw new Error(`目录不是 Git 仓库:${repoDir}`);
14
+ }
15
+ const meta = readRemixLocalMeta(repoDir);
16
+ assertPlaycraftProxyRemote(meta.cloneUrl);
17
+ const client = new PlayCraftHttpClient({
18
+ url: options.url,
19
+ token: options.token,
20
+ });
21
+ const gitArgs = options.gitArgs?.length ? options.gitArgs : [];
22
+ const effectiveArgs = gitArgs.length > 0
23
+ ? gitArgs
24
+ : (() => {
25
+ const upstream = spawnSync('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
26
+ cwd: repoDir,
27
+ encoding: 'utf8',
28
+ });
29
+ if (upstream.status === 0 && upstream.stdout.trim()) {
30
+ return [];
31
+ }
32
+ return ['origin', meta.branch];
33
+ })();
34
+ if (!options.json) {
35
+ process.stderr.write(`[pull] ${meta.cloneUrl}\n`);
36
+ process.stderr.write(`[pull] branch=${meta.branch} dir=${repoDir}\n`);
37
+ }
38
+ const result = runAuthenticatedGit(['pull', ...effectiveArgs], {
39
+ cwd: repoDir,
40
+ token: client.token,
41
+ pipe: Boolean(options.json),
42
+ });
43
+ if (result.status !== 0) {
44
+ const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
45
+ throw new Error(detail || `git pull 失败(exit ${result.status})`);
46
+ }
47
+ if (options.json) {
48
+ console.log(JSON.stringify({
49
+ schemaVersion: '1.0',
50
+ ok: true,
51
+ command: 'remix.pull',
52
+ requestId: client.requestId,
53
+ data: {
54
+ projectId: meta.projectId,
55
+ branch: meta.branch,
56
+ cloneUrl: meta.cloneUrl,
57
+ dir: repoDir,
58
+ gitArgs: effectiveArgs,
59
+ },
60
+ }));
61
+ return;
62
+ }
63
+ console.log(`✓ 已从 Remix 分支 ${meta.branch} 拉取`);
64
+ }
@@ -0,0 +1,70 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { PlayCraftHttpClient } from '../utils/playcraft-http-client.js';
5
+ import { assertPlaycraftProxyRemote, readRemixLocalMeta, runAuthenticatedGit, } from './local-git.js';
6
+ export { assertPlaycraftProxyRemote, readRemixLocalMeta } from './local-git.js';
7
+ /**
8
+ * Push 当前 Remix 仓库到 PlayCraft Git 代理。
9
+ *
10
+ * 故意不用 `git push` + credential.helper:Windows 上 Git Credential Manager
11
+ * 经常抢走鉴权并弹窗。这里与 clone 一样,用一次性 `http.extraHeader` 注入 PAT,
12
+ * 并清空本命令的 credential.helper,避免 GCM 介入。
13
+ */
14
+ export async function remixPush(options = {}) {
15
+ const repoDir = path.resolve(options.dir?.trim() || process.cwd());
16
+ if (!existsSync(path.join(repoDir, '.git'))) {
17
+ throw new Error(`目录不是 Git 仓库:${repoDir}`);
18
+ }
19
+ const meta = readRemixLocalMeta(repoDir);
20
+ assertPlaycraftProxyRemote(meta.cloneUrl);
21
+ const client = new PlayCraftHttpClient({
22
+ url: options.url,
23
+ token: options.token,
24
+ });
25
+ const gitArgs = options.gitArgs?.length ? options.gitArgs : [];
26
+ // 默认推当前分支;不强制改用户已设的 upstream。若无 upstream 且未传参数,
27
+ // 用 meta.branch 设 -u,避免「没有 upstream」报错。
28
+ const effectiveArgs = gitArgs.length > 0
29
+ ? gitArgs
30
+ : (() => {
31
+ const upstream = spawnSync('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
32
+ cwd: repoDir,
33
+ encoding: 'utf8',
34
+ });
35
+ if (upstream.status === 0 && upstream.stdout.trim()) {
36
+ return [];
37
+ }
38
+ return ['-u', 'origin', meta.branch];
39
+ })();
40
+ if (!options.json) {
41
+ process.stderr.write(`[push] ${meta.cloneUrl}\n`);
42
+ process.stderr.write(`[push] branch=${meta.branch} dir=${repoDir}\n`);
43
+ }
44
+ const result = runAuthenticatedGit(['push', ...effectiveArgs], {
45
+ cwd: repoDir,
46
+ token: client.token,
47
+ pipe: Boolean(options.json),
48
+ });
49
+ if (result.status !== 0) {
50
+ const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
51
+ throw new Error(detail || `git push 失败(exit ${result.status})`);
52
+ }
53
+ if (options.json) {
54
+ console.log(JSON.stringify({
55
+ schemaVersion: '1.0',
56
+ ok: true,
57
+ command: 'remix.push',
58
+ requestId: client.requestId,
59
+ data: {
60
+ projectId: meta.projectId,
61
+ branch: meta.branch,
62
+ cloneUrl: meta.cloneUrl,
63
+ dir: repoDir,
64
+ gitArgs: effectiveArgs,
65
+ },
66
+ }));
67
+ return;
68
+ }
69
+ console.log(`✓ 已推送到 Remix 分支 ${meta.branch}`);
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.0.56",
3
+ "version": "0.0.57",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,8 +45,8 @@
45
45
  "@gltf-transform/core": "^4.3.0",
46
46
  "@gltf-transform/extensions": "^4.3.0",
47
47
  "@gltf-transform/functions": "^4.3.0",
48
- "@playcraft/build": "^0.0.56",
49
- "@playcraft/common": "^0.0.56",
48
+ "@playcraft/build": "^0.0.57",
49
+ "@playcraft/common": "^0.0.57",
50
50
  "chokidar": "^4.0.3",
51
51
  "commander": "^13.1.0",
52
52
  "cors": "^2.8.6",