@pmate/cli 0.35.12 → 0.35.13

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
@@ -270,7 +270,7 @@ Meili's Host may pass `--actor-profile-id` with `PMATE_LOG_DELEGATE_TOKEN` in th
270
270
 
271
271
  Packaged installers are automatically uploaded to Aliyun OSS (`parrot-static`), using the same credentials as Android APK releases. The completion output and build report include public download URLs, SHA-256 checksums, and a `release.json` URL. Configure `oss.public.region` and `aliyun.ak/sk` in your PMate configuration. Conflicting published versions require an app version bump. `--dir` produces local unpacked output only; `--dry-run` does not upload.
272
272
 
273
- `pmate app build <ns:app> --target electron --config <file>` builds the configured web app, embeds its static output in an Electron ASAR, and produces a macOS DMG and ZIP. It does not require registry login. The first build downloads pinned Electron and electron-builder versions. Current targets: macOS `arm64` and `x64`, built on macOS.
273
+ `pmate app build <ns:app> --target electron --config <file>` builds the configured web app, embeds its static output in an Electron ASAR, and produces a macOS DMG and ZIP. Packaged releases require a CLI login with admin permission in the app namespace; `--dir` and `--dry-run` stay local. The first build downloads pinned Electron and electron-builder versions. Current targets: macOS and Windows, `arm64` and `x64`; macOS packages require a macOS build host.
274
274
 
275
275
  ```sh
276
276
  pmate app build pmate:hello --target electron --config apps/hello/desktop.config.json --platform mac --arch arm64 --out-dir release/desktop
@@ -294,12 +294,22 @@ Config (paths relative to the config file, `web.dist` relative to `web.cwd`):
294
294
 
295
295
  Use `--dry-run` to inspect the plan, `--dir` for an unpacked `.app`, or `--signed` to require a configured macOS signing identity. Default builds are ad-hoc signed for local use; they are not notarized distribution releases. Signed builds use electron-builder's standard signing/notarization environment variables; verify notarization separately before distributing.
296
296
 
297
- The shell serves the bundled renderer under its existing exact HTTPS origin, preserving authentication callbacks and API CORS. Other origins remain normal network requests. Static resources are hashed and checked at launch; missing assets return 404. The renderer is sandboxed with Node disabled and no privileged preload bridge. Sign-in, chat and inference still require internet. Updates require a new package. Each build prints its artifact directory and a resource/signing report; no build uploads or auto-updates occur.
297
+ The shell serves the bundled renderer under its existing exact HTTPS origin, preserving authentication callbacks and API CORS. Other origins remain normal network requests. Static resources are hashed and checked at launch; missing assets return 404. The renderer is sandboxed with Node disabled and no privileged preload bridge. Sign-in, chat and inference still require internet. The native application menu includes **检查更新…**. It queries apps-api, compares stable versions, and opens the official installer download after confirmation. Installation remains manual. Each build prints its artifact directory and a resource/signing report; packaged releases upload installers and register the version; `--dir` stays local.
298
298
 
299
299
  ### 日志统计默认口径
300
300
 
301
301
  请求次数与日均可使用 `pmate logs aggregate pmate:issues-api --metric count --topic application --field event=http.access --interval day --timezone Asia/Shanghai --from <ISO> --to <ISO> --json`。计数由数据库完成,返回 `count`、`durationDays`、`averagePerDay`,时间桶包含零值;不返回 UV 或延迟百分位。JSON 模式失败以非零状态退出并返回固定 `error.code`,区分权限、保留期、查询预算和上游故障,避免泄露上游原始错误。
302
302
 
303
+ ### Desktop version registry
304
+
305
+ Desktop clients need a stable way to discover releases without knowing their versioned OSS path. Apps-api owns this index; installer metadata and checksums remain in OSS `release.json`.
306
+
307
+ `GET https://apps-api.pmate.chat/versions/pmate:hello?platform=mac&arch=arm64` is public and returns `{ success: true, data: { app, version, platform, arch, manifestUrl, publishedAt, publishedBy, id } }`. A missing release returns 404; it does not mean the installed client is current. Supported targets are `mac|win` and `arm64|x64`. Only stable `major.minor.patch` versions are published.
308
+
309
+ After installers and their immutable manifest upload successfully, the build calls `POST /versions/:appRef` with `{ version, platform, arch, manifestUrl }` using the existing CLI session. Apps-api requires namespace admin permission and validates the official manifest identity and installer metadata. Older releases never supersede a higher version for the same target. `PMATE_APPS_API_BASE_URL` selects the publisher API; distributed clients query the production API.
310
+
311
+ A registration failure fails the release command, while preserving already uploaded installers. Once API access is repaired, retry the same POST with the manifest URL in the build report; do not rebuild different bytes under that version. Manual registration of an already published release is idempotent. Native checks have a timeout, reject mismatched targets and external download hosts, and never clear user data or restart the app.
312
+
303
313
  ### Android release signing
304
314
 
305
315
  `pmate service apk release <service> --env prod --variant release` resolves signing credentials from that service's server-side environment (host variables and local signing environment variables are not used). Configure `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, and `ANDROID_KEY_PASSWORD` as Service Secrets, plus `ANDROID_SIGNING_CERT_SHA256` as the expected certificate fingerprint. Android SDK `apksigner` must be installed. The CLI signs the Gradle release APK and verifies its certificate before uploading; missing credentials, invalid signatures, or fingerprint mismatches stop publication. Keystore files use restricted temporary storage and are removed after signing. Existing apps must retain their compatible signing key. Debug builds keep their existing Gradle signing behavior.
package/dist/index.js CHANGED
@@ -17543,12 +17543,15 @@ var import_node_os9 = require("os");
17543
17543
  var import_node_path24 = __toESM(require("path"));
17544
17544
 
17545
17545
  // src/desktopRelease.ts
17546
+ init_sessionStore();
17546
17547
  var import_node_crypto4 = require("crypto");
17547
17548
  var import_node_fs18 = require("fs");
17548
17549
  var import_node_path23 = __toESM(require("path"));
17549
17550
  async function uploadDesktopArtifacts(directory, app, version, platform, arch) {
17550
17551
  const segments = [...app.split(":"), version, platform, arch];
17551
17552
  if (app.split(":").length !== 2 || segments.some((s) => !/^[\w.-]+$/.test(s) || s === "." || s === "..")) throw new Error("Invalid desktop release identity");
17553
+ if (!["mac", "win"].includes(platform) || !["arm64", "x64"].includes(arch) || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version)) throw new Error("Desktop publication requires a stable version and supported target");
17554
+ const { token } = readRequiredSession({ preferredApp: DEFAULT_APP_ID });
17552
17555
  const bucket = "parrot-static";
17553
17556
  const prefix = `desktop/${segments.join("/")}/`;
17554
17557
  const url = (key) => `https://parrot-static.pmate.chat/${key.split("/").map(encodeURIComponent).join("/")}`;
@@ -17564,6 +17567,16 @@ async function uploadDesktopArtifacts(directory, app, version, platform, arch) {
17564
17567
  if (existing !== null && JSON.stringify(existing) !== JSON.stringify(release)) throw new Error("Desktop release already exists with different artifacts; bump the app version");
17565
17568
  for (const artifact of artifacts) await AliyunService.uploadFile(import_node_path23.default.join(directory, artifact.name), bucket, artifact.objectKey);
17566
17569
  await AliyunService.uploadJsonObject(bucket, manifestKey, release);
17570
+ const base = (process.env.PMATE_APPS_API_BASE_URL || "https://apps-api.pmate.chat").replace(/\/+$/, "");
17571
+ const response = await fetch(`${base}/versions/${encodeURIComponent(app)}`, {
17572
+ method: "POST",
17573
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
17574
+ body: JSON.stringify({ version, platform, arch, manifestUrl: url(manifestKey) }),
17575
+ signal: AbortSignal.timeout(3e4),
17576
+ redirect: "error"
17577
+ });
17578
+ const result = await response.json();
17579
+ if (!response.ok || result.success !== true) throw new Error(`Installers uploaded but version registration failed: ${result.message || response.status}. Retry this release before announcing it.`);
17567
17580
  return { bucket, manifestUrl: url(manifestKey), downloads: artifacts };
17568
17581
  }
17569
17582
 
@@ -1,3 +1,4 @@
1
+ const { createUpdateChecker } = require('./updates.cjs')
1
2
  const { app, BrowserWindow, session, shell, Menu, desktopCapturer, ipcMain, Notification, dialog } = require('electron')
2
3
  const path = require('node:path')
3
4
  const { installNotificationSettings } = require('./notification-settings.cjs')
@@ -60,9 +61,10 @@ else {
60
61
  return false
61
62
  })
62
63
  installNotificationSettings({ ipcMain, shell, origin: config.origin, getWindow: () => mainWindow })
64
+ const checkForUpdates = createUpdateChecker({ app, dialog, shell, appId: config.app })
63
65
  Menu.setApplicationMenu(Menu.buildFromTemplate([
64
66
  { role: 'appMenu' }, { role: 'editMenu' }, { role: 'viewMenu' }, { role: 'windowMenu' },
65
- { label: config.productName, submenu: [{ label: 'Back to app', click: () => { if (!mainWindow) createWindow(); else void mainWindow.loadURL(config.origin) } }] },
67
+ { label: config.productName, submenu: [{ label: '检查更新…', click: checkForUpdates }, { type: 'separator' }, { label: 'Back to app', click: () => { if (!mainWindow) createWindow(); else void mainWindow.loadURL(config.origin) } }] },
66
68
  ]))
67
69
  desktopNotifications = createDesktopNotifications({ ipcMain, Notification, origin: config.origin, appId: config.app, getWindow: () => mainWindow, showWindow: () => {
68
70
  if (!mainWindow) createWindow()
@@ -0,0 +1,51 @@
1
+ const API = 'https://apps-api.pmate.chat'
2
+ const CDN = 'https://parrot-static.pmate.chat'
3
+ const stable = value => typeof value === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value)
4
+ function compareVersions(a, b) {
5
+ if (!stable(a) || !stable(b)) throw new Error('Invalid stable version')
6
+ const left = a.split('.').map(BigInt), right = b.split('.').map(BigInt)
7
+ for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] > right[i] ? 1 : -1
8
+ return 0
9
+ }
10
+ async function findUpdate({ appId, currentVersion, platform, arch, fetchImpl = fetch }) {
11
+ const target = platform === 'darwin' ? 'mac' : platform === 'win32' ? 'win' : null
12
+ if (!target || !['arm64', 'x64'].includes(arch)) throw new Error('Unsupported update target')
13
+ const read = async url => {
14
+ const response = await fetchImpl(url, { signal: AbortSignal.timeout(15000), redirect: 'error', cache: 'no-store' })
15
+ if (!response.ok) throw new Error(`Update request failed (${response.status})`)
16
+ return response.json()
17
+ }
18
+ const envelope = await read(`${API}/versions/${encodeURIComponent(appId)}?platform=${target}&arch=${arch}`)
19
+ const release = envelope.data
20
+ if (envelope.success !== true || !release || release.app !== appId || release.platform !== target || release.arch !== arch || !stable(release.version)) throw new Error('Invalid version response')
21
+ if (compareVersions(release.version, currentVersion) <= 0) return null
22
+ const prefix = `${CDN}/desktop/${appId.split(':').map(encodeURIComponent).join('/')}/${release.version}/${target}/${arch}/`
23
+ if (release.manifestUrl !== prefix + 'release.json') throw new Error('Untrusted release manifest')
24
+ const manifest = await read(release.manifestUrl)
25
+ if (manifest.schemaVersion !== 1 || manifest.app !== appId || manifest.version !== release.version || manifest.platform !== target || manifest.arch !== arch || !Array.isArray(manifest.artifacts)) throw new Error('Release manifest mismatch')
26
+ const extension = target === 'mac' ? '.dmg' : '.exe'
27
+ const installer = manifest.artifacts.find(item => typeof item.name === 'string' && item.name.toLowerCase().endsWith(extension))
28
+ if (!installer || /[/\\]/.test(installer.name) || installer.downloadUrl !== prefix + encodeURIComponent(installer.name) || !/^[a-f0-9]{64}$/.test(installer.sha256) || !Number.isSafeInteger(installer.size) || installer.size <= 0) throw new Error('Invalid update installer')
29
+ return { version: release.version, ...installer }
30
+ }
31
+ function createUpdateChecker({ app, dialog, shell, appId, platform = process.platform, arch = process.arch, fetchImpl = fetch }) {
32
+ let checking = false
33
+ return async function checkForUpdates() {
34
+ if (checking) return
35
+ checking = true
36
+ try {
37
+ const currentVersion = app.getVersion()
38
+ const update = await findUpdate({ appId, currentVersion, platform, arch, fetchImpl })
39
+ if (!update) {
40
+ await dialog.showMessageBox({ type: 'info', title: '检查更新', message: '已是最新版本', detail: `当前版本:${currentVersion}`, buttons: ['确定'] })
41
+ return
42
+ }
43
+ const result = await dialog.showMessageBox({ type: 'info', title: '发现新版本', message: `${app.getName()} ${update.version}`, detail: `当前版本:${currentVersion}\n新版:${update.version}\n下载后打开安装包完成升级。`, buttons: ['下载新版', '稍后'], defaultId: 0, cancelId: 1 })
44
+ if (result.response === 0) await shell.openExternal(update.downloadUrl)
45
+ } catch (error) {
46
+ console.error('Desktop update check failed:', error)
47
+ await dialog.showMessageBox({ type: 'error', title: '检查更新失败', message: '无法检查更新,请稍后重试。', buttons: ['确定'] })
48
+ } finally { checking = false }
49
+ }
50
+ }
51
+ module.exports = { compareVersions, findUpdate, createUpdateChecker }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.35.12",
3
+ "version": "0.35.13",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {