@qmuse/qmuse-cli 1.0.3-dev.4 → 1.0.3-dev.6

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
@@ -50,7 +50,7 @@ qmuse import /path/to/react-project --space-id <spaceId>
50
50
 
51
51
  ### 更新已有应用
52
52
 
53
- 从 QMuse 下载的项目,在项目根目录执行 `qmuse import .` 即可更新对应应用;没有关联应用时会创建新应用。普通外部项目每次导入都会创建新应用。
53
+ 没有关联应用时,首次导入会创建新应用并自动保存本地绑定。之后在同一项目目录执行 `qmuse import .`,即可更新对应应用。请保留 `.qmuse/project.json`,从 QMuse 下载且带有绑定的项目也使用相同命令更新。
54
54
 
55
55
  平台已有新版本时,终端会询问是否用本地全部源码覆盖,不会自动合并平台修改。确认覆盖可执行 `qmuse import . --force`;脚本或 `--json` 模式须显式指定该参数。覆盖会在原应用创建新版本,仍需通过正常导入校验。
56
56
 
@@ -101,7 +101,7 @@ CLI 将本地 Runtime 的 `__MUSE__.domain` 设置为实际监听地址(如 `h
101
101
 
102
102
  服务端必须先部署 `/api/appwrite/local-dev/context` 和 `/api/appwrite/local-dev/mutation-events`,再发布并安装新版 CLI;缺少接口时不会降级为模拟云服务。
103
103
 
104
- 页面改动热更新,Schema/函数改动继续执行 `qmuse import .`。导入更新本地绑定后,连接会重新获取配置并刷新页面。切换登录账号或应用绑定需重启连接;退出 CLI 不会自动撤销浏览器已经建立的 AppBase Session。SDK 的匿名降级行为保持不变,验收必须检查实际 Session 用户,不能只看页面是否显示。
104
+ 页面改动热更新,Schema/函数改动继续执行 `qmuse import .`。CLI 启动期间设置 `QMUSE_HMR_GATE_ENABLED=false`,退出后恢复原值;项目的 `@qmuse/vite-config` 必须支持此开关,否则平台批量改码门禁仍会暂存本地修改。导入更新本地绑定后,连接会重新获取配置并刷新页面。切换登录账号或应用绑定需重启连接;退出 CLI 不会自动撤销浏览器已经建立的 AppBase Session。SDK 的匿名降级行为保持不变,验收必须检查实际 Session 用户,不能只看页面是否显示。
105
105
 
106
106
  联调验收包括 CRUD、刷新读回、文件上传/预览、函数调用和权限拒绝;浏览器 Cookie 限制需在目标浏览器验证。`.muse-tmp`、`.qmuse` 和系统临时配置不进入导入包。
107
107
 
package/dist/cli.js CHANGED
@@ -17,9 +17,10 @@ import { configureAppImport, getAppOpenUrl, getImportStatusUrl, queryAppImportSt
17
17
  import { pollAppImport } from './import/poll.js';
18
18
  import { preflightImport } from './import/preflight.js';
19
19
  import { resolveUpdateBase } from './import/update.js';
20
- import { clearPendingImport, completeManagedImport, loadManagedProject, saveManagedProject, setPendingImport, } from './import/project.js';
21
- import { clearImportFailureDiagnostic, writeImportFailureDiagnostic, } from './import/diagnostic.js';
22
- import { checkCloudInit, CLOUD_INIT_SUCCESS_MESSAGE } from './cloud.js';
20
+ import { clearPendingImport, completeManagedImport, createExternalProject, loadManagedProject, saveManagedProject, resolveProjectEnvironment, setPendingImport, } from './import/project.js';
21
+ import { clearImportFailureDiagnostic, writeImportFailureDiagnostic, importRetryGuidance, } from './import/diagnostic.js';
22
+ import { checkCloudInit, checkCloudEntitlement, CLOUD_INIT_SUCCESS_MESSAGE } from './cloud.js';
23
+ import { configurationGuidance } from './import/guidance.js';
23
24
  import { QMUSE_CLI_VERSION } from './version.js';
24
25
  import { startUpdateNotice } from './update-notice.js';
25
26
  import { startDevelopment } from './dev/index.js';
@@ -191,11 +192,14 @@ async function pollImportWithLogs(jobId, cfg, flags) {
191
192
  process.removeListener('SIGINT', onInterrupt);
192
193
  }
193
194
  }
194
- async function resolveImportConfiguration(status, cfg, flags) {
195
+ async function resolveImportConfiguration(status, cfg, flags, cloudTarget) {
195
196
  if (status.stage === 'QMUSE_CLOUD_ADAPTATION_REQUIRED')
196
197
  return undefined;
197
198
  const interactive = process.stdin.isTTY === true && flags['--json'] !== true;
198
199
  let cloudMode = parseImportCloudMode(str(flags, '--cloud-mode'));
200
+ if (cloudMode === 'AUTO' && ['QMUSE', 'EXTERNAL'].includes(status.cloudMode ?? '')) {
201
+ cloudMode = status.cloudMode;
202
+ }
199
203
  if (cloudMode === 'AUTO') {
200
204
  if (!interactive)
201
205
  return undefined;
@@ -235,6 +239,8 @@ async function resolveImportConfiguration(status, cfg, flags) {
235
239
  if (!acknowledgeEmptyCloudData)
236
240
  return undefined;
237
241
  }
242
+ if (cloudMode === 'QMUSE')
243
+ await checkCloudEntitlement(cloudTarget, cfg);
238
244
  const configured = await configureAppImport({
239
245
  jobId: status.jobId,
240
246
  cloudMode,
@@ -251,23 +257,34 @@ async function resolveImportConfiguration(status, cfg, flags) {
251
257
  }
252
258
  async function finishImport(status, sourceHash, cfg, flags, managedProject, rootDir) {
253
259
  if (status.status !== 'READY' || !status.appId || !status.versionId) {
260
+ const guidance = configurationGuidance(status, resolveProjectEnvironment(cfg, str(flags, '--env')), parseImportCloudMode(str(flags, '--cloud-mode')));
254
261
  if (flags['--json'] === true) {
255
- output({ success: false, ...status }, flags);
262
+ output({ success: false, ...status, ...(guidance ? { configurationRequired: guidance } : {}) }, flags);
256
263
  }
257
264
  else {
258
265
  printImportStatus(status, flags);
259
266
  if (status.status === 'FAILED')
260
267
  log.error('导入失败');
261
- else
262
- log.warn(`任务需要继续配置,可执行 qmuse import status ${status.jobId} --wait`);
268
+ else if (guidance) {
269
+ log.warn(guidance.message);
270
+ if (guidance.resumeCommand)
271
+ log.warn(`继续配置:${guidance.resumeCommand}`);
272
+ if (guidance.waitCommand)
273
+ log.warn(`配置后在原项目目录执行:${guidance.waitCommand}`);
274
+ }
263
275
  }
264
276
  process.exitCode = status.status === 'FAILED' ? 1 : 2;
265
277
  if (status.status === 'FAILED' && rootDir) {
266
- const diagnosticPath = await writeImportFailureDiagnostic({ rootDir, status });
278
+ const diagnosticProject = managedProject ?? await loadManagedProject(rootDir, cfg, str(flags, '--env'));
279
+ const diagnosticPath = await writeImportFailureDiagnostic({ rootDir, status, project: diagnosticProject,
280
+ environment: resolveProjectEnvironment(cfg, str(flags, '--env')) });
267
281
  if (flags['--json'] !== true) {
268
282
  log.warn(`诊断已写入 ${diagnosticPath}`);
269
283
  if (status.failureDiagnostic?.fixableByModel) {
270
- log.warn(`请让当前大模型读取 ${diagnosticPath},修复源码后执行 qmuse import .`);
284
+ const retry = importRetryGuidance(status, diagnosticProject);
285
+ log.warn(`请让当前大模型读取 ${diagnosticPath},修复源码。${retry.retryAdvice}`);
286
+ if (retry.retryCommand)
287
+ log.warn(retry.retryCommand);
271
288
  }
272
289
  }
273
290
  }
@@ -369,27 +386,53 @@ async function cmdImport(command, flags) {
369
386
  const mode = parseImportCloudMode(str(flags, '--cloud-mode'));
370
387
  if (mode === 'AUTO')
371
388
  fail('configure 必须指定 --cloud-mode EXTERNAL 或 QMUSE');
389
+ const current = await queryAppImportStatus(jobId, cfg);
390
+ if (!current.ok || !current.data)
391
+ throw new Error(`查询导入任务失败:${current.errorMessage ?? current.errorCode}`);
392
+ const guidance = configurationGuidance(current.data, resolveProjectEnvironment(cfg, str(flags, '--env')), mode);
393
+ if (mode === 'QMUSE' && guidance?.requiredFlags?.some(flag => flags[flag] !== true)) {
394
+ await finishImport(current.data, undefined, cfg, flags);
395
+ return;
396
+ }
397
+ if (mode === 'QMUSE') {
398
+ const project = await loadManagedProject(process.cwd(), cfg, str(flags, '--env'));
399
+ const pending = project?.manifest.pendingImports?.[project.environment];
400
+ const matchingProject = pending?.jobId === jobId ? project : undefined;
401
+ const appId = current.data.appId || matchingProject?.target?.appId;
402
+ const spaceId = pending?.jobId === jobId ? pending.spaceId : str(flags, '--space-id');
403
+ if (appId || spaceId)
404
+ await checkCloudEntitlement({ appId, spaceId: spaceId ?? '' }, cfg);
405
+ }
372
406
  const result = await configureAppImport({
373
407
  jobId,
374
408
  cloudMode: mode,
375
409
  ...(mode === 'EXTERNAL'
376
410
  ? { publicConfig: readPublicConfig(str(flags, '--public-config')) }
377
- : flags['--confirm-cloud-service'] === true
378
- ? { confirmCloudService: true }
379
- : { acknowledgeEmptyCloudData: flags['--acknowledge-empty-cloud-data'] === true }),
411
+ : {
412
+ confirmCloudService: flags['--confirm-cloud-service'] === true,
413
+ acknowledgeEmptyCloudData: flags['--acknowledge-empty-cloud-data'] === true,
414
+ }),
380
415
  }, cfg);
381
416
  if (!result.ok || !result.data) {
382
417
  fail(`提交导入配置失败:${result.errorMessage ?? result.errorCode ?? `HTTP ${result.status}`}`);
383
418
  }
384
- printImportStatus(result.data, flags);
419
+ if (['WAITING_CLOUD_CONFIRMATION', 'NEEDS_CONFIGURATION', 'FAILED'].includes(result.data.status)) {
420
+ await finishImport(result.data, undefined, cfg, flags);
421
+ }
422
+ else
423
+ printImportStatus(result.data, flags);
385
424
  return;
386
425
  }
387
426
  const directory = subcommand;
388
427
  if (!directory)
389
428
  fail('用法: qmuse import <directory> [--space-id <id>]');
390
429
  const rootDir = path.resolve(directory);
391
- await clearImportFailureDiagnostic(rootDir);
392
- const managedProject = await loadManagedProject(rootDir, cfg, str(flags, '--env'));
430
+ const managedProject = await loadManagedProject(rootDir, cfg, str(flags, '--env')) ??
431
+ createExternalProject(rootDir, cfg, str(flags, '--env'));
432
+ const pendingImport = managedProject.manifest.pendingImports?.[managedProject.environment];
433
+ if (pendingImport) {
434
+ throw new Error(`当前目录有尚未完成的导入任务,请先执行 qmuse import status ${pendingImport.jobId} --wait --env ${managedProject.environment},避免重复创建应用`);
435
+ }
393
436
  const explicitSpaceId = str(flags, '--space-id');
394
437
  const operation = managedProject?.target ? 'UPDATE' : 'CREATE';
395
438
  if (flags['--force'] === true && !managedProject?.target) {
@@ -435,6 +478,12 @@ async function cmdImport(command, flags) {
435
478
  log.success('源码没有变化,无需创建新版本');
436
479
  return;
437
480
  }
481
+ if (cloudMode === 'QMUSE') {
482
+ const quota = await checkCloudEntitlement({ appId: managedProject.target?.appId, spaceId }, cfg);
483
+ if (flags['--json'] !== true && quota.appLimit !== undefined) {
484
+ log.info(`QMuse 云服务额度:已使用 ${quota.enabledAppCount ?? 0}/${quota.appLimit}${quota.alreadyEnabled ? '(当前应用已开通)' : ''}`);
485
+ }
486
+ }
438
487
  if (flags['--json'] !== true) {
439
488
  log.success(`识别为 React 项目(${preflight.fileCount} 个文件)`);
440
489
  if (flags['--log'] === true) {
@@ -449,6 +498,7 @@ async function cmdImport(command, flags) {
449
498
  const archive = await createImportArchive(preflight);
450
499
  let sourceToken;
451
500
  try {
501
+ await saveManagedProject(managedProject);
452
502
  sourceToken = await uploadImportArchive({
453
503
  filePath: archive.filePath,
454
504
  spaceId,
@@ -467,7 +517,7 @@ async function cmdImport(command, flags) {
467
517
  sourceType: 'REACT_VITE',
468
518
  cloudMode,
469
519
  idempotencyKey: str(flags, '--idempotency-key'),
470
- sourceScenario: managedProject ? 'QMUSE_TEMPLATE' : 'EXTERNAL_REPOSITORY',
520
+ sourceScenario: managedProject.manifest.projectKind,
471
521
  operation,
472
522
  ...(managedProject?.target
473
523
  ? {
@@ -495,7 +545,7 @@ async function cmdImport(command, flags) {
495
545
  log.success(`已创建导入任务 ${jobId}`);
496
546
  let polled = await pollImportWithLogs(jobId, cfg, flags);
497
547
  while (polled.reason === 'CONFIGURATION_REQUIRED' && polled.status) {
498
- const configured = await resolveImportConfiguration(polled.status, cfg, flags);
548
+ const configured = await resolveImportConfiguration(polled.status, cfg, flags, { appId: managedProject.target?.appId, spaceId });
499
549
  if (!configured) {
500
550
  await finishImport(polled.status, preflight.sourceHash, cfg, flags, managedProject, rootDir);
501
551
  return;
@@ -769,7 +819,12 @@ async function main() {
769
819
  catch (err) {
770
820
  finishUpdateNotice();
771
821
  const msg = err instanceof Error ? err.message : String(err);
772
- fail(msg);
822
+ if (flags['--json'] === true) {
823
+ output({ success: false, message: msg }, flags);
824
+ process.exitCode = 1;
825
+ }
826
+ else
827
+ fail(msg);
773
828
  }
774
829
  finally {
775
830
  finishUpdateNotice();
package/dist/cloud.js CHANGED
@@ -20,6 +20,9 @@ export async function checkCloudInit(rootDir, cfg, explicitEnvironment) {
20
20
  if (!spaceId) {
21
21
  throw new Error('请先执行 qmuse space use <spaceId>,或通过 --space-id 指定目标空间');
22
22
  }
23
+ return checkCloudEntitlement({ appId, spaceId }, cfg);
24
+ }
25
+ export async function checkCloudEntitlement({ appId, spaceId }, cfg) {
23
26
  const response = appId
24
27
  ? await apiCall('POST', '/api/appwrite/entitlement/check', { appId }, cfg)
25
28
  : await apiCall('POST', '/api/appwrite/entitlement/space/check', { spaceId }, cfg);
package/dist/dev/index.js CHANGED
@@ -26,6 +26,7 @@ export async function startDevelopment(options) {
26
26
  const directory = await mkdtemp(path.join(tmpdir(), 'qmuse-local-dev-'));
27
27
  const runtimePath = path.join(directory, 'runtime.json');
28
28
  const previousPath = process.env.MUSE_RUNTIME_CONFIG_PATH;
29
+ const previousHmrGate = process.env.QMUSE_HMR_GATE_ENABLED;
29
30
  let server;
30
31
  let closed = false;
31
32
  const manifestPath = path.join(root, '.qmuse/project.json');
@@ -58,11 +59,16 @@ export async function startDevelopment(options) {
58
59
  delete process.env.MUSE_RUNTIME_CONFIG_PATH;
59
60
  else
60
61
  process.env.MUSE_RUNTIME_CONFIG_PATH = previousPath;
62
+ if (previousHmrGate === undefined)
63
+ delete process.env.QMUSE_HMR_GATE_ENABLED;
64
+ else
65
+ process.env.QMUSE_HMR_GATE_ENABLED = previousHmrGate;
61
66
  }
62
67
  };
63
68
  try {
64
69
  await writeRuntime();
65
70
  process.env.MUSE_RUNTIME_CONFIG_PATH = runtimePath;
71
+ process.env.QMUSE_HMR_GATE_ENABLED = 'false';
66
72
  const vite = await import(pathToFileURL(vitePath).href);
67
73
  server = await vite.createServer({
68
74
  root,
@@ -2,6 +2,15 @@ import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  export const QMUSE_IMPORT_DIAGNOSTIC_PATH = '.qmuse/last-import-error.json';
5
+ export function importRetryGuidance(status, project) {
6
+ const target = project?.target;
7
+ const pending = project?.manifest.pendingImports?.[project.environment];
8
+ const canRetryUpdate = target && status.operation !== 'CREATE' &&
9
+ (status.appId === target.appId || (!status.appId && pending?.jobId === status.jobId && pending.operation === 'UPDATE'));
10
+ return canRetryUpdate
11
+ ? { retryCommand: `qmuse import . --env ${project.environment}`, retryAdvice: '在当前项目目录修复源码后执行,将更新已绑定的应用。' }
12
+ : { retryCommand: null, retryAdvice: '当前任务没有可确认的原应用绑定。请先核对任务是否已创建应用,并恢复对应环境的项目绑定;直接重新 import 可能创建新应用。' };
13
+ }
5
14
  export async function writeImportFailureDiagnostic(input) {
6
15
  if (input.status.status !== 'FAILED') {
7
16
  throw new Error('只能为失败的导入任务写入诊断文件');
@@ -17,10 +26,18 @@ export async function writeImportFailureDiagnostic(input) {
17
26
  message: input.status.message,
18
27
  diagnostic: input.status.diagnostic,
19
28
  failureDiagnostic: input.status.failureDiagnostic,
20
- retryCommand: 'qmuse import .',
29
+ ...importRetryGuidance(input.status, input.project),
30
+ environment: input.project?.environment ?? input.environment,
21
31
  generatedAt: new Date().toISOString(),
22
32
  };
33
+ const historyPath = path.join(input.rootDir, '.qmuse', 'import-errors', input.project?.environment ?? input.environment ?? 'unknown', `${encodeURIComponent(input.status.jobId)}.json`);
34
+ await mkdir(path.dirname(historyPath), { recursive: true });
35
+ const historyTemporaryPath = `${historyPath}.${process.pid}.${randomUUID()}.tmp`;
23
36
  try {
37
+ await writeFile(historyTemporaryPath, `${JSON.stringify(payload, null, 2)}\n`, {
38
+ encoding: 'utf8', mode: 0o600, flag: 'wx',
39
+ });
40
+ await rename(historyTemporaryPath, historyPath);
24
41
  await writeFile(temporaryPath, `${JSON.stringify(payload, null, 2)}\n`, {
25
42
  encoding: 'utf8',
26
43
  mode: 0o600,
@@ -29,6 +46,7 @@ export async function writeImportFailureDiagnostic(input) {
29
46
  await rename(temporaryPath, filePath);
30
47
  }
31
48
  catch (error) {
49
+ await rm(historyTemporaryPath, { force: true });
32
50
  await rm(temporaryPath, { force: true });
33
51
  throw error;
34
52
  }
@@ -0,0 +1,35 @@
1
+ export function configurationGuidance(status, environment, requestedMode = 'AUTO') {
2
+ if (!['WAITING_CLOUD_CONFIRMATION', 'NEEDS_CONFIGURATION'].includes(status.status))
3
+ return undefined;
4
+ if (status.stage === 'QMUSE_CLOUD_ADAPTATION_REQUIRED') {
5
+ return { message: '请先根据诊断完成源码的 QMuse 云服务适配,再重新导入。' };
6
+ }
7
+ const mode = requestedMode === 'AUTO' ? status.cloudMode : requestedMode;
8
+ if (!/^[a-zA-Z0-9_-]+$/u.test(status.jobId))
9
+ return { message: '任务标识格式异常,请联系平台支持。' };
10
+ const base = `qmuse import configure ${status.jobId} --env ${environment}`;
11
+ const waitCommand = `qmuse import status ${status.jobId} --wait --env ${environment} --json`;
12
+ if (mode === 'QMUSE') {
13
+ const flag = status.sourceScenario === 'QMUSE_TEMPLATE'
14
+ ? '--confirm-cloud-service' : '--acknowledge-empty-cloud-data';
15
+ return {
16
+ message: `任务尚未完成,需要确认:模板使用 --confirm-cloud-service,外部项目使用 --acknowledge-empty-cloud-data。当前任务请使用 ${flag}。`,
17
+ requiredFlags: [flag],
18
+ resumeCommand: `${base} --cloud-mode QMUSE ${flag} --json`,
19
+ waitCommand,
20
+ };
21
+ }
22
+ if (mode === 'EXTERNAL') {
23
+ return {
24
+ message: `请提供外部云服务所需的公开配置:${(status.requiredConfigKeys ?? []).join(', ') || '请参考任务诊断'}。`,
25
+ requiredConfigKeys: status.requiredConfigKeys ?? [],
26
+ resumeCommand: `${base} --cloud-mode EXTERNAL --public-config @public-config.json --json`,
27
+ waitCommand,
28
+ };
29
+ }
30
+ return {
31
+ message: '请先选择 --cloud-mode QMUSE 或 EXTERNAL;QMUSE 模板需 --confirm-cloud-service,外部项目需 --acknowledge-empty-cloud-data。',
32
+ requiredFlags: ['--cloud-mode'],
33
+ waitCommand,
34
+ };
35
+ }
@@ -179,7 +179,7 @@ async function sha256File(filePath) {
179
179
  }
180
180
  export async function preflightImport(directory) {
181
181
  const rootDir = path.resolve(directory);
182
- const files = await collectFiles(rootDir);
182
+ const files = (await collectFiles(rootDir)).sort((left, right) => Buffer.compare(Buffer.from(left.relativePath), Buffer.from(right.relativePath)));
183
183
  const packageFile = files.find(file => file.relativePath === 'package.json');
184
184
  if (!packageFile)
185
185
  throw new Error('一期仅支持包含 package.json 的 React 项目');
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  export const QMUSE_PROJECT_PATH = '.qmuse/project.json';
5
5
  function isRecord(value) {
@@ -73,20 +73,21 @@ function parseManifest(value) {
73
73
  if (value.schemaVersion !== 1) {
74
74
  throw new Error(`${QMUSE_PROJECT_PATH} 的 schemaVersion 不受支持`);
75
75
  }
76
- if (value.projectKind !== 'QMUSE_TEMPLATE') {
77
- throw new Error(`${QMUSE_PROJECT_PATH} 的 projectKind 必须是 QMUSE_TEMPLATE`);
76
+ if (value.projectKind !== 'QMUSE_TEMPLATE' && value.projectKind !== 'EXTERNAL_REPOSITORY') {
77
+ throw new Error(`${QMUSE_PROJECT_PATH} 的 projectKind 无效`);
78
78
  }
79
79
  if (value.appType !== 'GENERAL') {
80
80
  throw new Error(`${QMUSE_PROJECT_PATH} 的 appType 必须是 GENERAL`);
81
81
  }
82
- if (!Number.isSafeInteger(value.packageVersion) || Number(value.packageVersion) < 1) {
82
+ const isTemplate = value.projectKind === 'QMUSE_TEMPLATE';
83
+ if (isTemplate && (!Number.isSafeInteger(value.packageVersion) || Number(value.packageVersion) < 1)) {
83
84
  throw new Error(`${QMUSE_PROJECT_PATH} 的 packageVersion 无效`);
84
85
  }
85
- if (!isRecord(value.skillVersions)) {
86
+ if (isTemplate && !isRecord(value.skillVersions)) {
86
87
  throw new Error(`${QMUSE_PROJECT_PATH} 的 skillVersions 必须是对象`);
87
88
  }
88
89
  const skillVersions = {};
89
- for (const [name, version] of Object.entries(value.skillVersions)) {
90
+ for (const [name, version] of Object.entries(isTemplate ? value.skillVersions : {})) {
90
91
  skillVersions[requireNonEmptyString(name, 'skillVersions key')] = requireNonEmptyString(version, `skillVersions.${name}`);
91
92
  }
92
93
  const defaultSpaceId = value.defaultSpaceId === undefined
@@ -94,11 +95,13 @@ function parseManifest(value) {
94
95
  : requireNonEmptyString(value.defaultSpaceId, 'defaultSpaceId');
95
96
  return {
96
97
  schemaVersion: 1,
97
- packageVersion: Number(value.packageVersion),
98
- projectKind: 'QMUSE_TEMPLATE',
98
+ projectKind: value.projectKind,
99
99
  appType: 'GENERAL',
100
- templateCommit: requireNonEmptyString(value.templateCommit, 'templateCommit'),
101
- skillVersions,
100
+ ...(isTemplate ? {
101
+ packageVersion: Number(value.packageVersion),
102
+ templateCommit: requireNonEmptyString(value.templateCommit, 'templateCommit'),
103
+ skillVersions,
104
+ } : {}),
102
105
  ...(defaultSpaceId ? { defaultSpaceId } : {}),
103
106
  targets: parseEnvironmentMap(value.targets, 'targets', parseTarget),
104
107
  ...(value.pendingImports === undefined
@@ -150,6 +153,7 @@ export async function loadManagedProject(rootDir, cfg, explicitEnvironment) {
150
153
  return { manifestPath, manifest, environment, target };
151
154
  }
152
155
  export async function saveManagedProject(context) {
156
+ await mkdir(path.dirname(context.manifestPath), { recursive: true });
153
157
  const temporaryPath = `${context.manifestPath}.${process.pid}.${randomUUID()}.tmp`;
154
158
  const content = `${JSON.stringify(context.manifest, null, 2)}\n`;
155
159
  try {
@@ -162,6 +166,18 @@ export async function saveManagedProject(context) {
162
166
  throw error;
163
167
  }
164
168
  }
169
+ export function createExternalProject(rootDir, cfg, explicitEnvironment) {
170
+ return {
171
+ manifestPath: path.join(rootDir, QMUSE_PROJECT_PATH),
172
+ environment: resolveProjectEnvironment(cfg, explicitEnvironment),
173
+ manifest: {
174
+ schemaVersion: 1,
175
+ projectKind: 'EXTERNAL_REPOSITORY',
176
+ appType: 'GENERAL',
177
+ targets: {},
178
+ },
179
+ };
180
+ }
165
181
  export function setPendingImport(context, pending) {
166
182
  context.manifest.pendingImports = {
167
183
  ...context.manifest.pendingImports,
@@ -2,7 +2,7 @@ export const SOURCE_MANIFEST_PATH = '.local/qmuse-import/source-manifest-v1.json
2
2
  export function createSourceManifest(preflight) {
3
3
  return {
4
4
  version: 1,
5
- algorithm: 'sha256-path-content-v1',
5
+ algorithm: 'sha256-path-content-v2',
6
6
  sourceHash: preflight.sourceHash,
7
7
  fileCount: preflight.fileCount,
8
8
  totalBytes: preflight.totalBytes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qmuse/qmuse-cli",
3
- "version": "1.0.3-dev.4",
3
+ "version": "1.0.3-dev.6",
4
4
  "description": "将本地 React 项目安全导入 QMuse 的命令行客户端",
5
5
  "type": "module",
6
6
  "bin": {