@synopackageland/cli 0.1.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 +13 -0
- package/dist/attestation/app-package.d.ts +86 -0
- package/dist/attestation/app-package.js +199 -0
- package/dist/attestation/install-on-host.d.ts +14 -0
- package/dist/attestation/install-on-host.js +52 -0
- package/dist/attestation/keyring.d.ts +21 -0
- package/dist/attestation/keyring.js +168 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +14 -0
- package/dist/build-spk/dsm-arch.d.ts +4 -0
- package/dist/build-spk/dsm-arch.js +32 -0
- package/dist/build-spk/dsm-icons.d.ts +2 -0
- package/dist/build-spk/dsm-icons.js +67 -0
- package/dist/build-spk/generator.d.ts +43 -0
- package/dist/build-spk/generator.js +653 -0
- package/dist/build-spk/native-payload.d.ts +22 -0
- package/dist/build-spk/native-payload.js +654 -0
- package/dist/build-spk/reproducible-archive.d.ts +8 -0
- package/dist/build-spk/reproducible-archive.js +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +345 -0
- package/dist/compose/analyze.d.ts +25 -0
- package/dist/compose/analyze.js +346 -0
- package/dist/compose/env.d.ts +7 -0
- package/dist/compose/env.js +36 -0
- package/dist/compose/privilege.d.ts +14 -0
- package/dist/compose/privilege.js +102 -0
- package/dist/compose/runner.d.ts +32 -0
- package/dist/compose/runner.js +84 -0
- package/dist/contract/load.d.ts +25 -0
- package/dist/contract/load.js +87 -0
- package/dist/contract/schema-validate.d.ts +3 -0
- package/dist/contract/schema-validate.js +50 -0
- package/dist/contract/validate.d.ts +3 -0
- package/dist/contract/validate.js +80 -0
- package/dist/contract/web-launch.d.ts +19 -0
- package/dist/contract/web-launch.js +94 -0
- package/dist/deploy/catalog-publish.d.ts +3 -0
- package/dist/deploy/catalog-publish.js +7 -0
- package/dist/deploy/deploy.d.ts +22 -0
- package/dist/deploy/deploy.js +136 -0
- package/dist/deploy/identity-shared.d.ts +3 -0
- package/dist/deploy/identity-shared.js +14 -0
- package/dist/deploy/identity.d.ts +7 -0
- package/dist/deploy/identity.js +20 -0
- package/dist/deploy/package-source.d.ts +7 -0
- package/dist/deploy/package-source.js +17 -0
- package/dist/deploy/run-as-values.d.ts +18 -0
- package/dist/deploy/run-as-values.js +48 -0
- package/dist/deploy/share-values.d.ts +20 -0
- package/dist/deploy/share-values.js +60 -0
- package/dist/deploy/spk-info.d.ts +12 -0
- package/dist/deploy/spk-info.js +41 -0
- package/dist/deploy/spk-version.d.ts +4 -0
- package/dist/deploy/spk-version.js +64 -0
- package/dist/dev.d.ts +13 -0
- package/dist/dev.js +31 -0
- package/dist/discovery.d.ts +8 -0
- package/dist/discovery.js +22 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +18 -0
- package/dist/host/broker-paths.d.ts +1 -0
- package/dist/host/broker-paths.js +1 -0
- package/dist/host/catalog-install.d.ts +59 -0
- package/dist/host/catalog-install.js +326 -0
- package/dist/host/credentials.d.ts +2 -0
- package/dist/host/credentials.js +37 -0
- package/dist/host/digests.d.ts +5 -0
- package/dist/host/digests.js +55 -0
- package/dist/host/dsm-client.d.ts +6 -0
- package/dist/host/dsm-client.js +290 -0
- package/dist/host/dsm-http.d.ts +1 -0
- package/dist/host/dsm-http.js +87 -0
- package/dist/host/fake-nas.d.ts +16 -0
- package/dist/host/fake-nas.js +116 -0
- package/dist/host/open-url.d.ts +5 -0
- package/dist/host/open-url.js +13 -0
- package/dist/host/share-lookup.d.ts +3 -0
- package/dist/host/share-lookup.js +144 -0
- package/dist/host/types.d.ts +67 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/info.d.ts +17 -0
- package/dist/info.js +34 -0
- package/dist/init.d.ts +4 -0
- package/dist/init.js +31 -0
- package/dist/inspect.d.ts +13 -0
- package/dist/inspect.js +102 -0
- package/dist/load-env.d.ts +1 -0
- package/dist/load-env.js +31 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.js +20 -0
- package/dist/mock-broker/bootstrap.d.ts +2 -0
- package/dist/mock-broker/bootstrap.js +8 -0
- package/dist/mock-broker/files.d.ts +60 -0
- package/dist/mock-broker/files.js +408 -0
- package/dist/mock-broker/oidc.d.ts +31 -0
- package/dist/mock-broker/oidc.js +161 -0
- package/dist/mock-broker/path.d.ts +9 -0
- package/dist/mock-broker/path.js +50 -0
- package/dist/mock-broker/server.d.ts +19 -0
- package/dist/mock-broker/server.js +350 -0
- package/dist/publish/build-ledger.d.ts +22 -0
- package/dist/publish/build-ledger.js +26 -0
- package/dist/publish/community-path.d.ts +6 -0
- package/dist/publish/community-path.js +41 -0
- package/dist/publish/curated-release.d.ts +78 -0
- package/dist/publish/curated-release.js +693 -0
- package/dist/publish/file-build-ledger.d.ts +18 -0
- package/dist/publish/file-build-ledger.js +268 -0
- package/dist/publish/github-pr.d.ts +25 -0
- package/dist/publish/github-pr.js +85 -0
- package/dist/publish/github-repo.d.ts +8 -0
- package/dist/publish/github-repo.js +21 -0
- package/dist/publish/publish.d.ts +23 -0
- package/dist/publish/publish.js +128 -0
- package/dist/publish/sync-definition.d.ts +4 -0
- package/dist/publish/sync-definition.js +48 -0
- package/dist/skill.d.ts +10 -0
- package/dist/skill.js +50 -0
- package/dist/templates/compose-import/compose.yaml +11 -0
- package/dist/templates/compose-import/synology-app.yaml +15 -0
- package/dist/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/dist/templates/hello-files/compose.yaml +8 -0
- package/dist/templates/hello-files/synology-app.yaml +13 -0
- package/dist/templates/hello-web/compose.yaml +7 -0
- package/dist/templates/hello-web/html/app.js +23 -0
- package/dist/templates/hello-web/html/index.html +15 -0
- package/dist/templates/hello-web/html/sdk.js +68 -0
- package/dist/templates/hello-web/synology-app.yaml +13 -0
- package/dist/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
- package/dist/templates/hello-web-page/compose.yaml +7 -0
- package/dist/templates/hello-web-page/html/app.js +23 -0
- package/dist/templates/hello-web-page/html/index.html +15 -0
- package/dist/templates/hello-web-page/html/sdk.js +68 -0
- package/dist/templates/hello-web-page/synology-app.yaml +13 -0
- package/dist/test-command.d.ts +13 -0
- package/dist/test-command.js +26 -0
- package/dist/test-host.d.ts +13 -0
- package/dist/test-host.js +24 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +1 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +103 -0
- package/package.json +38 -0
- package/schema/synology-app.schema.json +137 -0
- package/skills/SKILL.md +51 -0
- package/skills/reference/create.md +34 -0
- package/skills/reference/deploy.md +41 -0
- package/skills/reference/diagnose.md +42 -0
- package/skills/reference/files.md +37 -0
- package/skills/reference/identity.md +31 -0
- package/skills/reference/wrap.md +35 -0
- package/templates/compose-import/compose.yaml +11 -0
- package/templates/compose-import/synology-app.yaml +15 -0
- package/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/templates/hello-files/compose.yaml +8 -0
- package/templates/hello-files/synology-app.yaml +13 -0
- package/templates/hello-web/compose.yaml +7 -0
- package/templates/hello-web/html/app.js +23 -0
- package/templates/hello-web/html/index.html +15 -0
- package/templates/hello-web/html/sdk.js +68 -0
- package/templates/hello-web/synology-app.yaml +13 -0
- package/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
- package/templates/hello-web-page/compose.yaml +7 -0
- package/templates/hello-web-page/html/app.js +23 -0
- package/templates/hello-web-page/html/index.html +15 -0
- package/templates/hello-web-page/html/sdk.js +68 -0
- package/templates/hello-web-page/synology-app.yaml +13 -0
package/dist/skill.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const SKILL_CATALOG = [
|
|
5
|
+
{ id: "orchestrator", file: "SKILL.md", when: "永遠先讀" },
|
|
6
|
+
{ id: "create", file: "reference/create.md", when: "從零或一句話開始" },
|
|
7
|
+
{ id: "wrap", file: "reference/wrap.md", when: "已有 Compose 或要補契約" },
|
|
8
|
+
{ id: "identity", file: "reference/identity.md", when: "需要 DSM 登入者" },
|
|
9
|
+
{ id: "files", file: "reference/files.md", when: "需要 NAS 檔案" },
|
|
10
|
+
{ id: "deploy", file: "reference/deploy.md", when: "要放到 NAS" },
|
|
11
|
+
{ id: "diagnose", file: "reference/diagnose.md", when: "validate/deploy/Open 失敗" },
|
|
12
|
+
];
|
|
13
|
+
export function resolveSkillsRoot() {
|
|
14
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const candidates = [
|
|
16
|
+
join(moduleDir, "..", "skills"),
|
|
17
|
+
join(moduleDir, "skills"),
|
|
18
|
+
];
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
if (existsSync(join(candidate, "SKILL.md"))) {
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
throw new Error("找不到 Skill Pack。請重新安裝 @synopackageland/cli。");
|
|
25
|
+
}
|
|
26
|
+
export function listSkills() {
|
|
27
|
+
return [...SKILL_CATALOG];
|
|
28
|
+
}
|
|
29
|
+
export function skillPath(id) {
|
|
30
|
+
const entry = SKILL_CATALOG.find((item) => item.id === id);
|
|
31
|
+
if (!entry) {
|
|
32
|
+
throw new Error(`未知 skill:${id}`);
|
|
33
|
+
}
|
|
34
|
+
const path = join(resolveSkillsRoot(), entry.file);
|
|
35
|
+
if (!existsSync(path)) {
|
|
36
|
+
throw new Error(`Skill 檔案不存在:${entry.file}`);
|
|
37
|
+
}
|
|
38
|
+
return path;
|
|
39
|
+
}
|
|
40
|
+
export function readSkill(id) {
|
|
41
|
+
return readFileSync(skillPath(id), "utf8");
|
|
42
|
+
}
|
|
43
|
+
export function formatSkillList() {
|
|
44
|
+
const root = resolveSkillsRoot();
|
|
45
|
+
const lines = [`Skill Pack:${root}`, ""];
|
|
46
|
+
for (const entry of SKILL_CATALOG) {
|
|
47
|
+
lines.push(`${entry.id}\t${entry.when}\t${entry.file}`);
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
schema: 1
|
|
2
|
+
package: com.synology.compose.import
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
displayname: Compose Import
|
|
5
|
+
description: Start from an existing Compose file and select two Shared Folders at install time.
|
|
6
|
+
maintainer: Syno Package Land
|
|
7
|
+
os_min_ver: "7.2-69057"
|
|
8
|
+
|
|
9
|
+
install:
|
|
10
|
+
MEDIA_SHARE:
|
|
11
|
+
type: shared_folder
|
|
12
|
+
BACKUP_SHARE:
|
|
13
|
+
type: shared_folder
|
|
14
|
+
RUN_AS:
|
|
15
|
+
type: dsm_user
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
schema: 1
|
|
2
|
+
package: com.synology.hello.files
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
displayname: Hello Files
|
|
5
|
+
description: A minimal headless app with a Shared Folder mount.
|
|
6
|
+
maintainer: Syno Package Land
|
|
7
|
+
os_min_ver: "7.2-69057"
|
|
8
|
+
|
|
9
|
+
install:
|
|
10
|
+
MEDIA_SHARE:
|
|
11
|
+
type: shared_folder
|
|
12
|
+
RUN_AS:
|
|
13
|
+
type: dsm_user
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createSynologyClient } from "./sdk.js";
|
|
2
|
+
|
|
3
|
+
const status = document.getElementById("status");
|
|
4
|
+
const userEl = document.getElementById("user");
|
|
5
|
+
|
|
6
|
+
const client = createSynologyClient();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
await client.auth.requireLogin();
|
|
10
|
+
const user = await client.auth.getCurrentUser();
|
|
11
|
+
if (status) {
|
|
12
|
+
status.textContent = "Signed in with DSM";
|
|
13
|
+
}
|
|
14
|
+
if (userEl) {
|
|
15
|
+
userEl.textContent = user.username;
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
if (status) {
|
|
21
|
+
status.textContent = code === "UNAUTHENTICATED" ? "DSM login required" : message;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Hello Web</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>Hello Web</h1>
|
|
10
|
+
<p id="status">Opening with your DSM session…</p>
|
|
11
|
+
<p id="user"></p>
|
|
12
|
+
<script src="./bootstrap.js"></script>
|
|
13
|
+
<script type="module" src="./app.js"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function createSynologyClient(options = {}) {
|
|
2
|
+
const bootstrap =
|
|
3
|
+
options.bootstrap ??
|
|
4
|
+
globalThis.__SYNOLOGY_BOOTSTRAP__ ??
|
|
5
|
+
(options.brokerBaseUrl
|
|
6
|
+
? {
|
|
7
|
+
brokerBaseUrl: options.brokerBaseUrl,
|
|
8
|
+
jwksUrl: `${String(options.brokerBaseUrl).replace(/\/$/, "")}/.well-known/jwks.json`,
|
|
9
|
+
packageId: "com.synology.hello.web",
|
|
10
|
+
}
|
|
11
|
+
: undefined);
|
|
12
|
+
if (!bootstrap?.brokerBaseUrl) {
|
|
13
|
+
throw new Error("缺少 Platform SDK bootstrap。");
|
|
14
|
+
}
|
|
15
|
+
const apiBase = `${String(bootstrap.brokerBaseUrl).replace(/\/$/, "")}/api/v1`;
|
|
16
|
+
let accessToken = null;
|
|
17
|
+
|
|
18
|
+
async function parseJson(response) {
|
|
19
|
+
const text = await response.text();
|
|
20
|
+
const body = text ? JSON.parse(text) : {};
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const error = body.error ?? {};
|
|
23
|
+
const err = new Error(error.message ?? `Broker request failed with status ${response.status}`);
|
|
24
|
+
err.code = error.code;
|
|
25
|
+
err.details = error.details;
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
return body;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
auth: {
|
|
33
|
+
async requireLogin() {
|
|
34
|
+
const result = await parseJson(
|
|
35
|
+
await fetch(`${apiBase}/auth/login`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "content-type": "application/json" },
|
|
38
|
+
credentials: "include",
|
|
39
|
+
body: JSON.stringify({ packageId: bootstrap.packageId }),
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
accessToken = result.accessToken;
|
|
43
|
+
return result.user;
|
|
44
|
+
},
|
|
45
|
+
async getSession() {
|
|
46
|
+
if (!accessToken) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const result = await parseJson(
|
|
50
|
+
await fetch(`${apiBase}/session`, {
|
|
51
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
52
|
+
credentials: "include",
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
return result.user;
|
|
56
|
+
},
|
|
57
|
+
async getCurrentUser() {
|
|
58
|
+
const session = await this.getSession();
|
|
59
|
+
if (!session) {
|
|
60
|
+
const err = new Error("尚未建立有效身份。");
|
|
61
|
+
err.code = "UNAUTHENTICATED";
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
return session;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
schema: 1
|
|
2
|
+
package: com.synology.hello.web
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
displayname: Hello Web
|
|
5
|
+
description: A minimal Syno Package Land web app template.
|
|
6
|
+
maintainer: Syno Package Land
|
|
7
|
+
os_min_ver: "7.2-69057"
|
|
8
|
+
adminprotocol: http
|
|
9
|
+
adminport: 8080
|
|
10
|
+
adminurl: /
|
|
11
|
+
integration:
|
|
12
|
+
web:
|
|
13
|
+
launch: dsm-window
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createSynologyClient } from "./sdk.js";
|
|
2
|
+
|
|
3
|
+
const status = document.getElementById("status");
|
|
4
|
+
const userEl = document.getElementById("user");
|
|
5
|
+
|
|
6
|
+
const client = createSynologyClient();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
await client.auth.requireLogin();
|
|
10
|
+
const user = await client.auth.getCurrentUser();
|
|
11
|
+
if (status) {
|
|
12
|
+
status.textContent = "Signed in with DSM";
|
|
13
|
+
}
|
|
14
|
+
if (userEl) {
|
|
15
|
+
userEl.textContent = user.username;
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
if (status) {
|
|
21
|
+
status.textContent = code === "UNAUTHENTICATED" ? "DSM login required" : message;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Hello Web Page</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>Hello Web Page</h1>
|
|
10
|
+
<p id="status">Opening with your DSM session…</p>
|
|
11
|
+
<p id="user"></p>
|
|
12
|
+
<script src="./bootstrap.js"></script>
|
|
13
|
+
<script type="module" src="./app.js"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function createSynologyClient(options = {}) {
|
|
2
|
+
const bootstrap =
|
|
3
|
+
options.bootstrap ??
|
|
4
|
+
globalThis.__SYNOLOGY_BOOTSTRAP__ ??
|
|
5
|
+
(options.brokerBaseUrl
|
|
6
|
+
? {
|
|
7
|
+
brokerBaseUrl: options.brokerBaseUrl,
|
|
8
|
+
jwksUrl: `${String(options.brokerBaseUrl).replace(/\/$/, "")}/.well-known/jwks.json`,
|
|
9
|
+
packageId: "com.synology.hello.web.page",
|
|
10
|
+
}
|
|
11
|
+
: undefined);
|
|
12
|
+
if (!bootstrap?.brokerBaseUrl) {
|
|
13
|
+
throw new Error("缺少 Platform SDK bootstrap。");
|
|
14
|
+
}
|
|
15
|
+
const apiBase = `${String(bootstrap.brokerBaseUrl).replace(/\/$/, "")}/api/v1`;
|
|
16
|
+
let accessToken = null;
|
|
17
|
+
|
|
18
|
+
async function parseJson(response) {
|
|
19
|
+
const text = await response.text();
|
|
20
|
+
const body = text ? JSON.parse(text) : {};
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const error = body.error ?? {};
|
|
23
|
+
const err = new Error(error.message ?? `Broker request failed with status ${response.status}`);
|
|
24
|
+
err.code = error.code;
|
|
25
|
+
err.details = error.details;
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
return body;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
auth: {
|
|
33
|
+
async requireLogin() {
|
|
34
|
+
const result = await parseJson(
|
|
35
|
+
await fetch(`${apiBase}/auth/login`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "content-type": "application/json" },
|
|
38
|
+
credentials: "include",
|
|
39
|
+
body: JSON.stringify({ packageId: bootstrap.packageId }),
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
accessToken = result.accessToken;
|
|
43
|
+
return result.user;
|
|
44
|
+
},
|
|
45
|
+
async getSession() {
|
|
46
|
+
if (!accessToken) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const result = await parseJson(
|
|
50
|
+
await fetch(`${apiBase}/session`, {
|
|
51
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
52
|
+
credentials: "include",
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
return result.user;
|
|
56
|
+
},
|
|
57
|
+
async getCurrentUser() {
|
|
58
|
+
const session = await this.getSession();
|
|
59
|
+
if (!session) {
|
|
60
|
+
const err = new Error("尚未建立有效身份。");
|
|
61
|
+
err.code = "UNAUTHENTICATED";
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
return session;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
schema: 1
|
|
2
|
+
package: com.synology.hello.web.page
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
displayname: Hello Web Page
|
|
5
|
+
description: A Syno Package Land web app template that opens in a new browser page.
|
|
6
|
+
maintainer: Syno Package Land
|
|
7
|
+
os_min_ver: "7.2-69057"
|
|
8
|
+
adminprotocol: http
|
|
9
|
+
adminport: 8080
|
|
10
|
+
adminurl: /
|
|
11
|
+
integration:
|
|
12
|
+
web:
|
|
13
|
+
launch: new-page
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProjectRoot } from "./discovery.js";
|
|
2
|
+
import type { ComposeRunner } from "./compose/runner.js";
|
|
3
|
+
export interface TestProjectOptions {
|
|
4
|
+
project: ProjectRoot;
|
|
5
|
+
composeRunner?: ComposeRunner;
|
|
6
|
+
}
|
|
7
|
+
export interface TestProjectResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
composeInvoked: boolean;
|
|
10
|
+
errors: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function testProject(options: TestProjectOptions): Promise<TestProjectResult>;
|
|
13
|
+
export { materializeComposePaths, composeEnvDirsExist, prepareComposeEnvironment } from "./compose/env.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { materializeComposePaths, composeEnvDirsExist } from "./compose/env.js";
|
|
2
|
+
import { validateProject } from "./validate.js";
|
|
3
|
+
import { createDockerComposeRunner } from "./compose/runner.js";
|
|
4
|
+
export async function testProject(options) {
|
|
5
|
+
const validation = validateProject(options.project);
|
|
6
|
+
if (!validation.ok) {
|
|
7
|
+
return {
|
|
8
|
+
ok: false,
|
|
9
|
+
composeInvoked: false,
|
|
10
|
+
errors: validation.errors.map((e) => e.code),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
materializeComposePaths(options.project.root, options.project.composePath);
|
|
14
|
+
const composeRunner = options.composeRunner ?? createDockerComposeRunner();
|
|
15
|
+
await composeRunner.up(options.project.root);
|
|
16
|
+
await composeRunner.waitReady(options.project.root);
|
|
17
|
+
if (!composeEnvDirsExist(options.project.root)) {
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
composeInvoked: true,
|
|
21
|
+
errors: ["COMPOSE_ENV_MISSING"],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return { ok: true, composeInvoked: true, errors: [] };
|
|
25
|
+
}
|
|
26
|
+
export { materializeComposePaths, composeEnvDirsExist, prepareComposeEnvironment } from "./compose/env.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProjectRoot } from "./discovery.js";
|
|
2
|
+
import type { NasHost } from "./host/types.js";
|
|
3
|
+
export interface TestProjectOnHostOptions {
|
|
4
|
+
project: ProjectRoot;
|
|
5
|
+
host: NasHost;
|
|
6
|
+
dev?: boolean;
|
|
7
|
+
cookieHeader?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TestProjectOnHostResult {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function testProjectOnHost(options: TestProjectOnHostOptions): Promise<TestProjectOnHostResult>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { validateProject } from "./validate.js";
|
|
2
|
+
import { resolveDeployIdentity } from "./deploy/identity.js";
|
|
3
|
+
export async function testProjectOnHost(options) {
|
|
4
|
+
const validation = validateProject(options.project);
|
|
5
|
+
if (!validation.ok) {
|
|
6
|
+
return { ok: false, message: validation.errors.map((e) => e.code).join(", ") };
|
|
7
|
+
}
|
|
8
|
+
const identity = resolveDeployIdentity(options.project, { dev: options.dev });
|
|
9
|
+
const installed = await options.host.isPackageInstalled(identity.packageId);
|
|
10
|
+
if (!installed) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
message: `尚未部署到 ${options.host.dsmOrigin}。下一步:synopkgland deploy --host <nas>${options.dev ? " --dev" : ""}`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const smoke = await options.host.smokeRequireLogin({
|
|
17
|
+
packageIdentity: identity.packageId,
|
|
18
|
+
cookieHeader: options.cookieHeader ?? "id=fake; did=fake",
|
|
19
|
+
});
|
|
20
|
+
if (!smoke.ok) {
|
|
21
|
+
return { ok: false, message: "Open smoke 失敗:requireLogin 未取得 DSM 使用者。" };
|
|
22
|
+
}
|
|
23
|
+
return { ok: true };
|
|
24
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface SourceSpan {
|
|
2
|
+
file: string;
|
|
3
|
+
line: number;
|
|
4
|
+
column: number;
|
|
5
|
+
path?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ValidationError {
|
|
8
|
+
code: string;
|
|
9
|
+
span: SourceSpan;
|
|
10
|
+
message: string;
|
|
11
|
+
suggestion: string;
|
|
12
|
+
severity?: "error" | "warning";
|
|
13
|
+
}
|
|
14
|
+
export interface ValidationResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
errors: ValidationError[];
|
|
17
|
+
warnings?: ValidationError[];
|
|
18
|
+
}
|
|
19
|
+
export interface AppContract {
|
|
20
|
+
schema?: unknown;
|
|
21
|
+
package?: unknown;
|
|
22
|
+
version?: unknown;
|
|
23
|
+
displayname?: unknown;
|
|
24
|
+
description?: unknown;
|
|
25
|
+
maintainer?: unknown;
|
|
26
|
+
os_min_ver?: unknown;
|
|
27
|
+
adminport?: unknown;
|
|
28
|
+
adminprotocol?: unknown;
|
|
29
|
+
adminurl?: unknown;
|
|
30
|
+
install?: Record<string, InstallParameter>;
|
|
31
|
+
integration?: unknown;
|
|
32
|
+
permissions?: unknown;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface InstallParameter {
|
|
36
|
+
type?: string;
|
|
37
|
+
default?: unknown;
|
|
38
|
+
generate?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface ComposeService {
|
|
41
|
+
image?: string;
|
|
42
|
+
ports?: unknown;
|
|
43
|
+
volumes?: unknown;
|
|
44
|
+
environment?: unknown;
|
|
45
|
+
privileged?: boolean;
|
|
46
|
+
network_mode?: string;
|
|
47
|
+
devices?: unknown;
|
|
48
|
+
cap_add?: unknown;
|
|
49
|
+
pid?: string;
|
|
50
|
+
ipc?: string;
|
|
51
|
+
depends_on?: unknown;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
export interface ComposeDocument {
|
|
55
|
+
services?: Record<string, ComposeService>;
|
|
56
|
+
secrets?: Record<string, unknown>;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
export type TemplateName = "hello-web" | "hello-web-page" | "hello-files" | "compose-import";
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ValidationResult } from "./types.js";
|
|
2
|
+
import { type ProjectRoot } from "./discovery.js";
|
|
3
|
+
export type AnalyzerProfile = "personal" | "listing";
|
|
4
|
+
export declare function validateProject(project: ProjectRoot, options?: {
|
|
5
|
+
profile?: AnalyzerProfile;
|
|
6
|
+
}): ValidationResult;
|
|
7
|
+
export { findProjectRoot } from "./discovery.js";
|
|
8
|
+
export type { ProjectRoot } from "./discovery.js";
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { collectInstallParameters, loadContract } from "./contract/load.js";
|
|
3
|
+
import { validateContract } from "./contract/validate.js";
|
|
4
|
+
import { collectComposeInterpolations, collectVolumeSourceInterpolations, loadCompose, validateAdminPortMapping, validateComposeHostBinds, validateComposeRunAs, validateComposeSecurity, } from "./compose/analyze.js";
|
|
5
|
+
import { COMPOSE_FILENAME, CONTRACT_FILENAME } from "./discovery.js";
|
|
6
|
+
import { declaredPrivilegeFlags } from "./compose/privilege.js";
|
|
7
|
+
export function validateProject(project, options = {}) {
|
|
8
|
+
const profile = options.profile ?? "personal";
|
|
9
|
+
const errors = [];
|
|
10
|
+
const contractFile = CONTRACT_FILENAME;
|
|
11
|
+
const composeFile = COMPOSE_FILENAME;
|
|
12
|
+
const parsed = loadContract(project.contractPath);
|
|
13
|
+
errors.push(...validateContract(parsed, contractFile));
|
|
14
|
+
if (!existsSync(project.composePath)) {
|
|
15
|
+
errors.push({
|
|
16
|
+
code: "COMPOSE_MISSING",
|
|
17
|
+
span: { file: composeFile, line: 1, column: 1 },
|
|
18
|
+
message: "找不到 compose.yaml。",
|
|
19
|
+
suggestion: "請在專案根目錄建立 compose.yaml。",
|
|
20
|
+
});
|
|
21
|
+
return { ok: false, errors };
|
|
22
|
+
}
|
|
23
|
+
const parsedCompose = loadCompose(project.composePath);
|
|
24
|
+
if (!parsedCompose) {
|
|
25
|
+
return { ok: false, errors };
|
|
26
|
+
}
|
|
27
|
+
errors.push(...validateComposeSecurity(parsedCompose.compose, composeFile, {
|
|
28
|
+
profile,
|
|
29
|
+
declaredPrivilege: declaredPrivilegeFlags(parsed.contract),
|
|
30
|
+
}));
|
|
31
|
+
const installParams = collectInstallParameters(parsed.contract);
|
|
32
|
+
const sharedFolderParams = new Set(installParams.filter(({ param }) => param.type === "shared_folder").map(({ name }) => name));
|
|
33
|
+
const installParamNames = new Set(installParams.map(({ name }) => name));
|
|
34
|
+
errors.push(...validateComposeHostBinds(parsedCompose.compose, composeFile, {
|
|
35
|
+
sharedFolderParams,
|
|
36
|
+
installParamNames,
|
|
37
|
+
}));
|
|
38
|
+
if (typeof parsed.contract.adminport === "number") {
|
|
39
|
+
errors.push(...validateAdminPortMapping(parsed.contract.adminport, parsedCompose.compose, composeFile));
|
|
40
|
+
}
|
|
41
|
+
const interpolations = collectComposeInterpolations(parsedCompose.compose);
|
|
42
|
+
const volumeSourceVars = collectVolumeSourceInterpolations(parsedCompose.compose);
|
|
43
|
+
const dsmUserParams = installParams.filter(({ param }) => param.type === "dsm_user");
|
|
44
|
+
const hasSharedFolder = sharedFolderParams.size > 0;
|
|
45
|
+
if (dsmUserParams.length > 1) {
|
|
46
|
+
errors.push({
|
|
47
|
+
code: "CONTRACT_DSM_USER_DUPLICATE",
|
|
48
|
+
span: { file: contractFile, line: 1, column: 1, path: "install" },
|
|
49
|
+
message: "每顆 App 至多一個 dsm_user。",
|
|
50
|
+
suggestion: "請只保留一個 Container Run-as 參數。",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (hasSharedFolder && dsmUserParams.length === 0) {
|
|
54
|
+
errors.push({
|
|
55
|
+
code: "DSM_USER_REQUIRED",
|
|
56
|
+
span: { file: contractFile, line: 1, column: 1, path: "install" },
|
|
57
|
+
message: "有 Shared Folder 時必須宣告恰好一個 dsm_user 作為 Container Run-as。",
|
|
58
|
+
suggestion: "請新增 install.<NAME>.type: dsm_user,Compose 引用 ${NAME_UID}:${NAME_GID}。",
|
|
59
|
+
severity: profile === "listing" ? "error" : "warning",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
errors.push(...validateComposeRunAs(parsedCompose.compose, composeFile, { hasSharedFolder }).map((item) => item.code === "COMPOSE_USER_ROOT_FORBIDDEN"
|
|
63
|
+
? { ...item, severity: (profile === "listing" ? "error" : "warning") }
|
|
64
|
+
: item));
|
|
65
|
+
for (const { name, param } of installParams) {
|
|
66
|
+
const referenced = param.type === "shared_folder" ? volumeSourceVars.has(name) : interpolations.has(name);
|
|
67
|
+
if ((param.type === "string" || param.type === "boolean" || param.type === "shared_folder") &&
|
|
68
|
+
!referenced) {
|
|
69
|
+
errors.push({
|
|
70
|
+
code: "INSTALL_PARAMETER_UNREFERENCED",
|
|
71
|
+
span: {
|
|
72
|
+
file: contractFile,
|
|
73
|
+
line: 1,
|
|
74
|
+
column: 1,
|
|
75
|
+
path: `install.${name}`,
|
|
76
|
+
},
|
|
77
|
+
message: `Install Parameter「${name}」未在 Compose 中被引用。`,
|
|
78
|
+
suggestion: `請在 compose.yaml 以 \${${name}} 引用此參數。`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (param.type === "dsm_user") {
|
|
82
|
+
const uidName = `${name}_UID`;
|
|
83
|
+
const gidName = `${name}_GID`;
|
|
84
|
+
if (!interpolations.has(uidName) || !interpolations.has(gidName)) {
|
|
85
|
+
errors.push({
|
|
86
|
+
code: "INSTALL_DSM_USER_UNREFERENCED",
|
|
87
|
+
span: {
|
|
88
|
+
file: contractFile,
|
|
89
|
+
line: 1,
|
|
90
|
+
column: 1,
|
|
91
|
+
path: `install.${name}`,
|
|
92
|
+
},
|
|
93
|
+
message: `dsm_user「${name}」必須在 Compose 以 \${${uidName}} 與 \${${gidName}} 引用。`,
|
|
94
|
+
suggestion: `請把容器 user 或 PUID/PGID 設成 \${${uidName}}:\${${gidName}}。`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const warnings = errors.filter((item) => item.severity === "warning");
|
|
100
|
+
const hardErrors = errors.filter((item) => item.severity !== "warning");
|
|
101
|
+
return { ok: hardErrors.length === 0, errors: hardErrors, warnings };
|
|
102
|
+
}
|
|
103
|
+
export { findProjectRoot } from "./discovery.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@synopackageland/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Syno Package Land Developer Kit CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"synopkgland": "dist/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates",
|
|
12
|
+
"schema",
|
|
13
|
+
"skills"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc && node scripts/copy-templates.mjs",
|
|
17
|
+
"test": "npm run build && node --import tsx --test test/**/*.test.ts",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@synopackageland/dev-sdk": "0.1.0",
|
|
26
|
+
"@synopackageland/package-source": "0.1.0",
|
|
27
|
+
"ajv": "^8.17.1",
|
|
28
|
+
"commander": "^13.1.0",
|
|
29
|
+
"jose": "^6.0.10",
|
|
30
|
+
"tar": "^7.4.3",
|
|
31
|
+
"yaml": "^2.7.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.13.10",
|
|
35
|
+
"tsx": "^4.19.3",
|
|
36
|
+
"typescript": "^5.8.2"
|
|
37
|
+
}
|
|
38
|
+
}
|