@synopackageland/cli 0.1.0 → 0.3.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.
Files changed (49) hide show
  1. package/dist/bin.js +3 -5
  2. package/dist/build-spk/broker-glue.d.ts +16 -0
  3. package/dist/build-spk/broker-glue.js +40 -0
  4. package/dist/build-spk/delete-app-data.d.ts +11 -0
  5. package/dist/build-spk/delete-app-data.js +56 -0
  6. package/dist/build-spk/generator.js +123 -70
  7. package/dist/build-spk/native-lifecycle.d.ts +51 -0
  8. package/dist/build-spk/native-lifecycle.js +205 -0
  9. package/dist/build-spk/native-payload.d.ts +15 -0
  10. package/dist/build-spk/native-payload.js +65 -39
  11. package/dist/cli.js +47 -8
  12. package/dist/deploy/deploy.d.ts +3 -0
  13. package/dist/deploy/deploy.js +13 -0
  14. package/dist/discovery.d.ts +2 -0
  15. package/dist/discovery.js +2 -0
  16. package/dist/host/digests.js +9 -1
  17. package/dist/host/dsm-client.d.ts +7 -0
  18. package/dist/host/dsm-client.js +25 -0
  19. package/dist/host/types.d.ts +6 -0
  20. package/dist/init.js +1 -0
  21. package/dist/native/load.d.ts +8 -0
  22. package/dist/native/load.js +11 -0
  23. package/dist/native/types.d.ts +28 -0
  24. package/dist/native/types.js +3 -0
  25. package/dist/native/validate.d.ts +6 -0
  26. package/dist/native/validate.js +144 -0
  27. package/dist/publish/curated-release.js +277 -91
  28. package/dist/publish/preflight.d.ts +24 -0
  29. package/dist/publish/preflight.js +116 -0
  30. package/dist/publish/publish.d.ts +4 -0
  31. package/dist/publish/publish.js +13 -5
  32. package/dist/release-bin.d.ts +2 -0
  33. package/dist/release-bin.js +19 -0
  34. package/dist/templates/native/native.yaml +16 -0
  35. package/dist/templates/native/synology-app.yaml +7 -0
  36. package/dist/types.d.ts +1 -1
  37. package/dist/validate.js +37 -4
  38. package/package.json +9 -8
  39. package/skills/reference/diagnose.md +15 -1
  40. package/templates/native/native.yaml +16 -0
  41. package/templates/native/synology-app.yaml +7 -0
  42. package/dist/deploy/package-source.d.ts +0 -7
  43. package/dist/deploy/package-source.js +0 -17
  44. package/dist/deploy/spk-info.d.ts +0 -12
  45. package/dist/deploy/spk-info.js +0 -41
  46. package/dist/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
  47. package/dist/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
  48. package/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
  49. package/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
@@ -0,0 +1,16 @@
1
+ schema: 1
2
+
3
+ targets:
4
+ x86_64:
5
+ path: ./dist/app-x86_64.tar.gz
6
+ strip_components: 1
7
+ aarch64:
8
+ path: ./dist/app-aarch64.tar.gz
9
+ strip_components: 1
10
+
11
+ service:
12
+ exec: bin/app
13
+ args:
14
+ - "-data=${SYNOPKG_PKGVAR}"
15
+ env:
16
+ TMPDIR: ${SYNOPKG_PKGVAR}/tmp
@@ -0,0 +1,7 @@
1
+ schema: 1
2
+ package: com.example.native-app
3
+ version: 0.1.0
4
+ displayname: Native App
5
+ description: A minimal Syno Package Land native binary app template.
6
+ maintainer: Syno Package Land
7
+ os_min_ver: "7.2-60000"
package/dist/types.d.ts CHANGED
@@ -56,4 +56,4 @@ export interface ComposeDocument {
56
56
  secrets?: Record<string, unknown>;
57
57
  [key: string]: unknown;
58
58
  }
59
- export type TemplateName = "hello-web" | "hello-web-page" | "hello-files" | "compose-import";
59
+ export type TemplateName = "hello-web" | "hello-web-page" | "hello-files" | "compose-import" | "native";
package/dist/validate.js CHANGED
@@ -1,25 +1,58 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
2
3
  import { collectInstallParameters, loadContract } from "./contract/load.js";
3
4
  import { validateContract } from "./contract/validate.js";
4
5
  import { collectComposeInterpolations, collectVolumeSourceInterpolations, loadCompose, validateAdminPortMapping, validateComposeHostBinds, validateComposeRunAs, validateComposeSecurity, } from "./compose/analyze.js";
5
- import { COMPOSE_FILENAME, CONTRACT_FILENAME } from "./discovery.js";
6
+ import { loadNative } from "./native/load.js";
7
+ import { validateNative } from "./native/validate.js";
8
+ import { COMPOSE_FILENAME, CONTRACT_FILENAME, NATIVE_FILENAME, } from "./discovery.js";
6
9
  import { declaredPrivilegeFlags } from "./compose/privilege.js";
7
10
  export function validateProject(project, options = {}) {
8
11
  const profile = options.profile ?? "personal";
9
12
  const errors = [];
10
13
  const contractFile = CONTRACT_FILENAME;
11
14
  const composeFile = COMPOSE_FILENAME;
15
+ const nativeFile = NATIVE_FILENAME;
12
16
  const parsed = loadContract(project.contractPath);
13
17
  errors.push(...validateContract(parsed, contractFile));
14
- if (!existsSync(project.composePath)) {
18
+ const hasCompose = existsSync(project.composePath);
19
+ const nativePath = project.nativePath ?? join(project.root, NATIVE_FILENAME);
20
+ const hasNative = existsSync(nativePath);
21
+ // 2. native.yaml 與 compose.yaml 互斥:兩個都有 → 拒絕;兩個都沒有 → 拒絕。
22
+ if (hasCompose && hasNative) {
23
+ errors.push({
24
+ code: "PROJECT_RUNTIME_EXCLUSIVE",
25
+ span: { file: nativeFile, line: 1, column: 1 },
26
+ message: "專案同時存在 compose.yaml 與 native.yaml,兩者互斥。",
27
+ suggestion: "請選擇使用 compose.yaml(容器 App)或 native.yaml(原生二進位 App),並刪除另一個檔案。",
28
+ });
29
+ return { ok: false, errors };
30
+ }
31
+ if (!hasCompose && !hasNative) {
15
32
  errors.push({
16
33
  code: "COMPOSE_MISSING",
17
34
  span: { file: composeFile, line: 1, column: 1 },
18
- message: "找不到 compose.yaml。",
19
- suggestion: "請在專案根目錄建立 compose.yaml。",
35
+ message: "找不到 compose.yaml 或 native.yaml。",
36
+ suggestion: "請在專案根目錄建立 compose.yaml 或 native.yaml。",
20
37
  });
21
38
  return { ok: false, errors };
22
39
  }
40
+ if (hasNative) {
41
+ const parsedNative = loadNative(nativePath);
42
+ if (!parsedNative) {
43
+ errors.push({
44
+ code: "NATIVE_LOAD_FAILED",
45
+ span: { file: nativeFile, line: 1, column: 1 },
46
+ message: "無法讀取 native.yaml。",
47
+ suggestion: "請確認 native.yaml 存在且格式為合法 YAML。",
48
+ });
49
+ return { ok: false, errors };
50
+ }
51
+ errors.push(...validateNative(parsedNative, nativeFile, { profile }));
52
+ const warnings = errors.filter((item) => item.severity === "warning");
53
+ const hardErrors = errors.filter((item) => item.severity !== "warning");
54
+ return { ok: hardErrors.length === 0, errors: hardErrors, warnings };
55
+ }
23
56
  const parsedCompose = loadCompose(project.composePath);
24
57
  if (!parsedCompose) {
25
58
  return { ok: false, errors };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
- "synopkgland": "dist/bin.js"
7
+ "synopkgland": "./dist/bin.js",
8
+ "synopkgland-release": "./dist/release-bin.js"
8
9
  },
9
10
  "files": [
10
11
  "dist",
@@ -17,13 +18,9 @@
17
18
  "test": "npm run build && node --import tsx --test test/**/*.test.ts",
18
19
  "prepublishOnly": "npm run build"
19
20
  },
20
- "publishConfig": {
21
- "access": "public",
22
- "registry": "https://registry.npmjs.org"
23
- },
24
21
  "dependencies": {
25
- "@synopackageland/dev-sdk": "0.1.0",
26
- "@synopackageland/package-source": "0.1.0",
22
+ "@synopackageland/dev-sdk": "^0.2.0",
23
+ "@synopackageland/package-source": "^0.2.0",
27
24
  "ajv": "^8.17.1",
28
25
  "commander": "^13.1.0",
29
26
  "jose": "^6.0.10",
@@ -34,5 +31,9 @@
34
31
  "@types/node": "^22.13.10",
35
32
  "tsx": "^4.19.3",
36
33
  "typescript": "^5.8.2"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "registry": "https://registry.npmjs.org"
37
38
  }
38
39
  }
@@ -10,7 +10,21 @@
10
10
  | `CONTRACT_SCHEMA_UNSUPPORTED` | `schema` 必須為整數 `1` |
11
11
  | `CONTRACT_INVALID_STRING`/`CONTRACT_ADMINPORT_INVALID` | 修正字串或 `adminport` 型別 |
12
12
  | `INSTALL_STRING_*`/`INSTALL_PARAMETER_UNREFERENCED` | 修正 install 預設值;在 compose 加 `${參數名}` |
13
- | `COMPOSE_MISSING` | 建立 `compose.yaml` |
13
+ | `COMPOSE_MISSING` | 建立 `compose.yaml`(容器 App)或 `native.yaml`(原生 binary App) |
14
+ | `PROJECT_RUNTIME_EXCLUSIVE` | `compose.yaml` 與 `native.yaml` 互斥,刪掉其中一個 |
15
+ | `NATIVE_SCHEMA_INVALID` | `native.yaml` 的 `schema` 只接受 `1` |
16
+ | `NATIVE_TARGETS_REQUIRED` / `NATIVE_TARGETS_EMPTY` | 在 `targets` 底下至少宣告一個 `x86_64` 或 `aarch64` |
17
+ | `NATIVE_ARCH_UNKNOWN` | `targets` 的 key 只接受 `x86_64`、`aarch64` |
18
+ | `NATIVE_TARGET_SOURCE_EXCLUSIVE` | 每個 target 只能有 `path:` 或 `url:` 其中一個 |
19
+ | `NATIVE_PATH_FORBIDDEN_IN_LISTING` | Curated 上架只接受 `url:` + `sha256:`;本機開發才可以用 `path:` |
20
+ | `NATIVE_SHA256_REQUIRED_IN_LISTING` | 補上 `sha256:`,CI 要能自己重抓比對 |
21
+ | `NATIVE_SHA256_INVALID` | `sha256` 必須是 64 個 hex 字元 |
22
+ | `NATIVE_STRIP_COMPONENTS_INVALID` | `strip_components` 必須是 >= 0 的整數 |
23
+ | `NATIVE_EXEC_REQUIRED` / `NATIVE_EXEC_INVALID_PATH` | `service.exec` 必填,且必須是相對 `SYNOPKG_PKGDEST` 的路徑,不可為絕對路徑或含 `..` |
24
+ | `NATIVE_PKGVAR_REQUIRED` | `service.args` 或 `service.env` 至少要用一次 `${SYNOPKG_PKGVAR}`,否則升級時資料會被連同 `target/` 一起換掉 |
25
+ | `NATIVE_SERVICE_USER_FORBIDDEN` | 拿掉 `service.user`:DSM 自己建 package user,`SYNOPKG_PKGVAR` 已經是對的 owner |
26
+ | `NATIVE_SERVICE_SHELL_FIELD_FORBIDDEN` | 拿掉 `command:` / `pre_start:` 這類 shell 欄位;`args` 是陣列,逐項傳給 exec,不經 shell |
27
+ | `NATIVE_ARGS_INVALID` / `NATIVE_ENV_INVALID` | `args` 必須是字串陣列,`env` 必須是 string→string |
14
28
  | `COMPOSE_PRIVILEGED_FORBIDDEN` 等安全碼 | 移除 privileged、host 網路、socket 掛載等 |
15
29
  | `COMPOSE_SERVICE_NOT_FOUND` | 修正 `adminport` 對應的 service |
16
30
  | `CONTRACT_ADMINPORT_UNMAPPED` | 在 compose 發布與 `adminport` 相同的 host port |
@@ -0,0 +1,16 @@
1
+ schema: 1
2
+
3
+ targets:
4
+ x86_64:
5
+ path: ./dist/app-x86_64.tar.gz
6
+ strip_components: 1
7
+ aarch64:
8
+ path: ./dist/app-aarch64.tar.gz
9
+ strip_components: 1
10
+
11
+ service:
12
+ exec: bin/app
13
+ args:
14
+ - "-data=${SYNOPKG_PKGVAR}"
15
+ env:
16
+ TMPDIR: ${SYNOPKG_PKGVAR}/tmp
@@ -0,0 +1,7 @@
1
+ schema: 1
2
+ package: com.example.native-app
3
+ version: 0.1.0
4
+ displayname: Native App
5
+ description: A minimal Syno Package Land native binary app template.
6
+ maintainer: Syno Package Land
7
+ os_min_ver: "7.2-60000"
@@ -1,7 +0,0 @@
1
- import { DEFAULT_PACKAGE_SOURCE_ORIGIN, fetchPackageSourceManifest, publishSpkToSource, resolvePublishTarget, type PublishTarget, type PublishSpkResult } from "@synology/package-source/publish-spk";
2
- import { resolveNextSpkVersion } from "@synology/package-source/next-version";
3
- import type { IndexManifest } from "@synology/package-source/index-manifest";
4
- export { DEFAULT_PACKAGE_SOURCE_ORIGIN, fetchPackageSourceManifest, publishSpkToSource, resolvePublishTarget, resolveNextSpkVersion, };
5
- export type { PublishTarget, PublishSpkResult, IndexManifest };
6
- export declare function resolvePackageSourceManifest(origin?: string): Promise<IndexManifest | null>;
7
- export declare function publishBuiltSpk(spkPath: string, target?: PublishTarget): Promise<PublishSpkResult>;
@@ -1,17 +0,0 @@
1
- import { DEFAULT_PACKAGE_SOURCE_ORIGIN, fetchPackageSourceManifest, publishSpkToSource, resolvePublishTarget, } from "@synology/package-source/publish-spk";
2
- import { resolveNextSpkVersion } from "@synology/package-source/next-version";
3
- export { DEFAULT_PACKAGE_SOURCE_ORIGIN, fetchPackageSourceManifest, publishSpkToSource, resolvePublishTarget, resolveNextSpkVersion, };
4
- const LIVE_TEST_FALLBACK_ORIGIN = "https://syno-package-source-throwaway.bidacumen.workers.dev";
5
- export async function resolvePackageSourceManifest(origin = resolvePublishTarget().origin) {
6
- const manifest = await fetchPackageSourceManifest(origin);
7
- if (manifest) {
8
- return manifest;
9
- }
10
- if (origin !== LIVE_TEST_FALLBACK_ORIGIN) {
11
- return fetchPackageSourceManifest(LIVE_TEST_FALLBACK_ORIGIN);
12
- }
13
- return null;
14
- }
15
- export async function publishBuiltSpk(spkPath, target = resolvePublishTarget()) {
16
- return publishSpkToSource(spkPath, target);
17
- }
@@ -1,12 +0,0 @@
1
- export interface SpkInfo {
2
- package: string;
3
- version: string;
4
- displayname?: string;
5
- description?: string;
6
- maintainer?: string;
7
- install_dep_packages?: string;
8
- os_min_ver?: string;
9
- startable?: boolean;
10
- }
11
- export declare function parseSpkInfoContent(info: string): SpkInfo;
12
- export declare function readSpkInfo(spkPath: string): Promise<SpkInfo>;
@@ -1,41 +0,0 @@
1
- import { readFileSync, rmSync } from "node:fs";
2
- import { mkdtempSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { extractSpk } from "../build-spk/generator.js";
6
- function parseInfoField(info, key) {
7
- const match = new RegExp(`^${key}="((?:\\\\.|[^"\\\\])*)"`, "m").exec(info);
8
- if (!match) {
9
- return undefined;
10
- }
11
- return match[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
12
- }
13
- export function parseSpkInfoContent(info) {
14
- const packageName = parseInfoField(info, "package");
15
- const version = parseInfoField(info, "version");
16
- if (!packageName || !version) {
17
- throw new Error("SPK INFO missing package or version");
18
- }
19
- const startableRaw = parseInfoField(info, "startable");
20
- return {
21
- package: packageName,
22
- version,
23
- displayname: parseInfoField(info, "displayname"),
24
- description: parseInfoField(info, "description"),
25
- maintainer: parseInfoField(info, "maintainer"),
26
- install_dep_packages: parseInfoField(info, "install_dep_packages"),
27
- os_min_ver: parseInfoField(info, "os_min_ver"),
28
- startable: startableRaw === undefined ? undefined : startableRaw === "yes",
29
- };
30
- }
31
- export async function readSpkInfo(spkPath) {
32
- const dir = mkdtempSync(join(tmpdir(), "syno-spk-info-"));
33
- try {
34
- await extractSpk(spkPath, dir);
35
- const info = readFileSync(join(dir, "INFO"), "utf8");
36
- return parseSpkInfoContent(info);
37
- }
38
- finally {
39
- rmSync(dir, { recursive: true, force: true });
40
- }
41
- }