@pmate/cli 0.35.12 → 0.36.0
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 +12 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +28 -5
- package/dist/templates/electron/main.cjs +3 -1
- package/dist/templates/electron/updates.cjs +51 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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.d.ts
CHANGED
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
|
|
|
@@ -18921,7 +18934,7 @@ async function queryDeveloperLogs(input, fetcher = fetch) {
|
|
|
18921
18934
|
if (!Number.isSafeInteger(input.page) || input.page < 0) throw new Error("Invalid log page");
|
|
18922
18935
|
if (input.sinceMs !== void 0 && (!Number.isSafeInteger(input.sinceMs) || input.sinceMs < 0)) throw new Error("Invalid log time");
|
|
18923
18936
|
if (input.keyword && input.keyword.length > 256) throw new Error("Log search must be at most 256 characters");
|
|
18924
|
-
const params = new URLSearchParams({ service: input.service, page: String(input.page), topic: input.topic ?? "application" });
|
|
18937
|
+
const params = new URLSearchParams({ service: input.service, page: String(input.page), category: input.category ?? "app", topic: input.topic ?? (input.category === "web" ? "pv" : "application") });
|
|
18925
18938
|
if (input.environment) params.set("environment", input.environment);
|
|
18926
18939
|
if (input.fields) params.set("fields", JSON.stringify(input.fields));
|
|
18927
18940
|
if (input.keyword) params.set("keyword", input.keyword);
|
|
@@ -20001,6 +20014,7 @@ var ServiceRuntimeRunner = class {
|
|
|
20001
20014
|
service: `${namespace}:${appName}`,
|
|
20002
20015
|
page: options.page,
|
|
20003
20016
|
keyword: options.search,
|
|
20017
|
+
category: options.category,
|
|
20004
20018
|
topic: options.topic,
|
|
20005
20019
|
fields: options.fields,
|
|
20006
20020
|
environment: options.envName,
|
|
@@ -20692,8 +20706,8 @@ var ServiceRuntimeRunner = class {
|
|
|
20692
20706
|
}
|
|
20693
20707
|
};
|
|
20694
20708
|
function resolveLogChainId(env = process.env) {
|
|
20695
|
-
if (env.APP_LOG_CHAIN_ID && env.APP_LOG_CHAIN_ID !== "
|
|
20696
|
-
return "
|
|
20709
|
+
if (env.APP_LOG_CHAIN_ID && env.APP_LOG_CHAIN_ID !== "app-logs") throw new Error("Logs require log chain");
|
|
20710
|
+
return "app-logs";
|
|
20697
20711
|
}
|
|
20698
20712
|
function shellQuote2(value) {
|
|
20699
20713
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -20905,7 +20919,7 @@ var ServiceCommandHandler = class {
|
|
|
20905
20919
|
}
|
|
20906
20920
|
case "log": {
|
|
20907
20921
|
if (args.app) {
|
|
20908
|
-
const result = await queryDeveloperLogs({ app: args.app, service: `${args.namespace}:${args.name}`, page: args.page, keyword: args.search, sinceMs: args.sinceMs, topic: args.topic, fields: args.fields, environment: args.env });
|
|
20922
|
+
const result = await queryDeveloperLogs({ app: args.app, service: `${args.namespace}:${args.name}`, page: args.page, keyword: args.search, sinceMs: args.sinceMs, category: args.category, topic: args.topic, fields: args.fields, environment: args.env });
|
|
20909
20923
|
console.log(JSON.stringify(result, null, args.json ? void 0 : 2));
|
|
20910
20924
|
return;
|
|
20911
20925
|
}
|
|
@@ -20917,6 +20931,7 @@ var ServiceCommandHandler = class {
|
|
|
20917
20931
|
search: args.search,
|
|
20918
20932
|
follow: args.follow,
|
|
20919
20933
|
envName: args.env,
|
|
20934
|
+
category: args.category,
|
|
20920
20935
|
topic: args.topic,
|
|
20921
20936
|
fields: args.fields,
|
|
20922
20937
|
sinceMs: args.sinceMs,
|
|
@@ -24199,6 +24214,13 @@ function parseServiceArgs(tokens) {
|
|
|
24199
24214
|
while (rest.length > 0) {
|
|
24200
24215
|
const token = rest.shift();
|
|
24201
24216
|
switch (token) {
|
|
24217
|
+
case "--category": {
|
|
24218
|
+
if (action !== "log") throw new Error(`Unknown service ${action} option: ${token}`);
|
|
24219
|
+
const value = rest.shift();
|
|
24220
|
+
if (value !== "app" && value !== "web") throw new Error("Invalid --category");
|
|
24221
|
+
logFilters.category = value;
|
|
24222
|
+
break;
|
|
24223
|
+
}
|
|
24202
24224
|
case "--topic": {
|
|
24203
24225
|
if (action !== "log") throw new Error(`Unknown service ${action} option: ${token}`);
|
|
24204
24226
|
const value = requireValue4(rest, token);
|
|
@@ -24952,7 +24974,7 @@ function parseLogsArgs(input) {
|
|
|
24952
24974
|
const params = {}, fields = {};
|
|
24953
24975
|
let json = false, actorProfileId;
|
|
24954
24976
|
if (rest[0] && !rest[0].startsWith("--")) params.service = parseNamespacedRef(rest.shift(), { entityLabel: "service" }).fullId;
|
|
24955
|
-
const keys = { "--metric": "metric", "--topic": "topic", "--env": "environment", "--search": "keyword", "--group-by": "groupBy", "--interval": "interval", "--timezone": "timezone", "--request-id": "requestId", "--page": "page" };
|
|
24977
|
+
const keys = { "--category": "category", "--metric": "metric", "--topic": "topic", "--env": "environment", "--search": "keyword", "--group-by": "groupBy", "--interval": "interval", "--timezone": "timezone", "--request-id": "requestId", "--page": "page" };
|
|
24956
24978
|
const times = { "--from": "from", "--to": "to", "--compare-from": "compareFrom", "--compare-to": "compareTo" };
|
|
24957
24979
|
while (rest.length) {
|
|
24958
24980
|
const flag = rest.shift();
|
|
@@ -24984,6 +25006,7 @@ function parseLogsArgs(input) {
|
|
|
24984
25006
|
} else if (keys[flag]) params[keys[flag]] = value;
|
|
24985
25007
|
else throw Error(`Unknown log option: ${flag}`);
|
|
24986
25008
|
}
|
|
25009
|
+
if (params.category && !["app", "web"].includes(params.category)) throw Error("Invalid --category");
|
|
24987
25010
|
if (tool !== "schema" && !params.service) throw Error("Service reference required");
|
|
24988
25011
|
if (tool === "schema" && (Object.keys(params).length || Object.keys(fields).length)) throw Error("logs schema only accepts --json");
|
|
24989
25012
|
if (tool === "aggregate" || tool === "compare") {
|
|
@@ -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 }
|