@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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BuildLedger, BuildLedgerSnapshot } from "./build-ledger.js";
|
|
2
|
+
export declare const BUILD_LEDGER_FILENAME = "package-build-ledger.jsonl";
|
|
3
|
+
export declare const BUILD_LEDGER_PATH_ENV = "SYNOPKGLAND_BUILD_LEDGER_PATH";
|
|
4
|
+
export interface ResolveBuildLedgerPathOptions {
|
|
5
|
+
env?: NodeJS.Dict<string>;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
communityAppsRoot?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function defaultBuildLedgerPath(communityAppsRoot: string): string;
|
|
10
|
+
export declare function resolveBuildLedgerPath(options?: ResolveBuildLedgerPathOptions): string;
|
|
11
|
+
export declare class FileBuildLedger implements BuildLedger {
|
|
12
|
+
readonly ledgerPath: string;
|
|
13
|
+
constructor(ledgerPath: string);
|
|
14
|
+
allocate(packageIdentity: string): number;
|
|
15
|
+
markPublishFailed(packageIdentity: string, build: number): void;
|
|
16
|
+
snapshot(packageIdentity: string): BuildLedgerSnapshot;
|
|
17
|
+
}
|
|
18
|
+
export declare function runFileBuildLedgerCli(argv: string[], env?: NodeJS.Dict<string>): void;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable append-only Package Build ledger for protected-main merge CI.
|
|
3
|
+
*
|
|
4
|
+
* Default path: synopkgland-community-apps/ci/package-build-ledger.jsonl
|
|
5
|
+
* (override with SYNOPKGLAND_BUILD_LEDGER_PATH). Concurrent allocate() is
|
|
6
|
+
* serialized with an exclusive lockfile next to the ledger. Do not commit live
|
|
7
|
+
* numbers; Git history / release artifacts remain disaster recovery.
|
|
8
|
+
*
|
|
9
|
+
* This module does not sign, upload artifacts, or mutate Package Source index.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync, } from "node:fs";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
+
import { resolveCommunityAppsRoot } from "./community-path.js";
|
|
16
|
+
export const BUILD_LEDGER_FILENAME = "package-build-ledger.jsonl";
|
|
17
|
+
export const BUILD_LEDGER_PATH_ENV = "SYNOPKGLAND_BUILD_LEDGER_PATH";
|
|
18
|
+
const LOCK_RETRY_MS = 15;
|
|
19
|
+
const LOCK_TIMEOUT_MS = 30_000;
|
|
20
|
+
const LOCK_FRESH_MS = 2_000;
|
|
21
|
+
export function defaultBuildLedgerPath(communityAppsRoot) {
|
|
22
|
+
return join(communityAppsRoot, "ci", BUILD_LEDGER_FILENAME);
|
|
23
|
+
}
|
|
24
|
+
export function resolveBuildLedgerPath(options = {}) {
|
|
25
|
+
const env = options.env ?? process.env;
|
|
26
|
+
const fromEnv = env[BUILD_LEDGER_PATH_ENV]?.trim();
|
|
27
|
+
if (fromEnv) {
|
|
28
|
+
return resolve(fromEnv);
|
|
29
|
+
}
|
|
30
|
+
const cwd = options.cwd ?? process.cwd();
|
|
31
|
+
const communityRoot = options.communityAppsRoot ?? resolveCommunityAppsRoot(cwd);
|
|
32
|
+
if (communityRoot) {
|
|
33
|
+
return defaultBuildLedgerPath(communityRoot);
|
|
34
|
+
}
|
|
35
|
+
return resolve(cwd, "synopkgland-community-apps", "ci", BUILD_LEDGER_FILENAME);
|
|
36
|
+
}
|
|
37
|
+
export class FileBuildLedger {
|
|
38
|
+
ledgerPath;
|
|
39
|
+
constructor(ledgerPath) {
|
|
40
|
+
this.ledgerPath = ledgerPath;
|
|
41
|
+
}
|
|
42
|
+
allocate(packageIdentity) {
|
|
43
|
+
requirePackageIdentity(packageIdentity);
|
|
44
|
+
return withLedgerLock(this.ledgerPath, () => {
|
|
45
|
+
const records = readRecords(this.ledgerPath);
|
|
46
|
+
const build = lastAllocated(records, packageIdentity) + 1;
|
|
47
|
+
appendRecord(this.ledgerPath, {
|
|
48
|
+
packageIdentity,
|
|
49
|
+
build,
|
|
50
|
+
event: "allocated",
|
|
51
|
+
at: new Date().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
return build;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
markPublishFailed(packageIdentity, build) {
|
|
57
|
+
requirePackageIdentity(packageIdentity);
|
|
58
|
+
withLedgerLock(this.ledgerPath, () => {
|
|
59
|
+
appendRecord(this.ledgerPath, {
|
|
60
|
+
packageIdentity,
|
|
61
|
+
build,
|
|
62
|
+
event: "publish-failed",
|
|
63
|
+
at: new Date().toISOString(),
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
snapshot(packageIdentity) {
|
|
68
|
+
requirePackageIdentity(packageIdentity);
|
|
69
|
+
return withLedgerLock(this.ledgerPath, () => snapshotFromRecords(readRecords(this.ledgerPath), packageIdentity));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function runFileBuildLedgerCli(argv, env = process.env) {
|
|
73
|
+
const command = argv[2];
|
|
74
|
+
const ledger = new FileBuildLedger(resolveBuildLedgerPath({ env }));
|
|
75
|
+
if (command === "allocate") {
|
|
76
|
+
const packageIdentity = requirePackageIdentity(argv[3]);
|
|
77
|
+
const build = ledger.allocate(packageIdentity);
|
|
78
|
+
process.stdout.write(`${JSON.stringify({ packageIdentity, build })}\n`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (command === "mark-failed") {
|
|
82
|
+
const packageIdentity = requirePackageIdentity(argv[3]);
|
|
83
|
+
const build = Number(argv[4]);
|
|
84
|
+
if (!Number.isInteger(build) || build < 1) {
|
|
85
|
+
throw new Error("usage: file-build-ledger mark-failed <packageIdentity> <build>");
|
|
86
|
+
}
|
|
87
|
+
ledger.markPublishFailed(packageIdentity, build);
|
|
88
|
+
process.stdout.write(`${JSON.stringify({ packageIdentity, build, event: "publish-failed" })}\n`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (command === "snapshot") {
|
|
92
|
+
const packageIdentity = requirePackageIdentity(argv[3]);
|
|
93
|
+
process.stdout.write(`${JSON.stringify({ packageIdentity, ...ledger.snapshot(packageIdentity) })}\n`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
throw new Error("usage: file-build-ledger <allocate|mark-failed|snapshot> <packageIdentity> [build]");
|
|
97
|
+
}
|
|
98
|
+
function requirePackageIdentity(packageIdentity) {
|
|
99
|
+
if (typeof packageIdentity !== "string" || packageIdentity.trim().length === 0) {
|
|
100
|
+
throw new Error("package identity is required");
|
|
101
|
+
}
|
|
102
|
+
return packageIdentity;
|
|
103
|
+
}
|
|
104
|
+
function snapshotFromRecords(records, packageIdentity) {
|
|
105
|
+
const consumed = records
|
|
106
|
+
.filter((record) => record.packageIdentity === packageIdentity && record.event === "allocated")
|
|
107
|
+
.map((record) => record.build);
|
|
108
|
+
return {
|
|
109
|
+
nextBuild: lastAllocated(records, packageIdentity),
|
|
110
|
+
consumed,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function lastAllocated(records, packageIdentity) {
|
|
114
|
+
let max = 0;
|
|
115
|
+
for (const record of records) {
|
|
116
|
+
if (record.packageIdentity === packageIdentity && record.event === "allocated" && record.build > max) {
|
|
117
|
+
max = record.build;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return max;
|
|
121
|
+
}
|
|
122
|
+
function readRecords(ledgerPath) {
|
|
123
|
+
if (!existsSync(ledgerPath)) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const text = readFileSync(ledgerPath, "utf8");
|
|
127
|
+
const records = [];
|
|
128
|
+
for (const [index, line] of text.split("\n").entries()) {
|
|
129
|
+
if (line.trim().length === 0) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
records.push(JSON.parse(line));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new Error(`invalid ledger record at ${ledgerPath}:${index + 1}: ${String(error)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return records;
|
|
140
|
+
}
|
|
141
|
+
function appendRecord(ledgerPath, record) {
|
|
142
|
+
mkdirSync(dirname(ledgerPath), { recursive: true });
|
|
143
|
+
const fd = openSync(ledgerPath, "a");
|
|
144
|
+
try {
|
|
145
|
+
writeSync(fd, `${JSON.stringify(record)}\n`);
|
|
146
|
+
fsyncSync(fd);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
closeSync(fd);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function withLedgerLock(ledgerPath, fn) {
|
|
153
|
+
const lockPath = `${ledgerPath}.lock`;
|
|
154
|
+
mkdirSync(dirname(ledgerPath), { recursive: true });
|
|
155
|
+
const held = acquireExclusiveLock(lockPath);
|
|
156
|
+
try {
|
|
157
|
+
return fn();
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
releaseLock(held.fd, lockPath, held.token);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function acquireExclusiveLock(lockPath) {
|
|
164
|
+
const started = Date.now();
|
|
165
|
+
for (;;) {
|
|
166
|
+
const token = randomUUID();
|
|
167
|
+
try {
|
|
168
|
+
const fd = openSync(lockPath, "wx");
|
|
169
|
+
writeSync(fd, `${process.pid} ${token}\n`);
|
|
170
|
+
fsyncSync(fd);
|
|
171
|
+
return { fd, token };
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (!isAlreadyExists(error)) {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
if (Date.now() - started > LOCK_TIMEOUT_MS) {
|
|
178
|
+
throw new Error(`timed out acquiring ledger lock: ${lockPath}`);
|
|
179
|
+
}
|
|
180
|
+
stealIfStale(lockPath);
|
|
181
|
+
sleepSync(LOCK_RETRY_MS);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function stealIfStale(lockPath) {
|
|
186
|
+
try {
|
|
187
|
+
const ageMs = Date.now() - statSync(lockPath).mtimeMs;
|
|
188
|
+
if (ageMs < LOCK_FRESH_MS) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const parsed = parseLockOwner(readFileSync(lockPath, "utf8"));
|
|
192
|
+
if (parsed && pidIsAlive(parsed.pid) && ageMs < LOCK_TIMEOUT_MS) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
unlinkSync(lockPath);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (!isNotFound(error)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function parseLockOwner(raw) {
|
|
204
|
+
const match = raw.trim().match(/^(\d+)\s+(\S+)$/);
|
|
205
|
+
if (!match) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const pid = Number(match[1]);
|
|
209
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return { pid, token: match[2] };
|
|
213
|
+
}
|
|
214
|
+
function releaseLock(fd, lockPath, token) {
|
|
215
|
+
try {
|
|
216
|
+
closeSync(fd);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
try {
|
|
220
|
+
const parsed = parseLockOwner(readFileSync(lockPath, "utf8"));
|
|
221
|
+
if (parsed?.token === token) {
|
|
222
|
+
unlinkSync(lockPath);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Best effort; a leftover lock is stolen if the holder is gone.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function pidIsAlive(pid) {
|
|
231
|
+
try {
|
|
232
|
+
process.kill(pid, 0);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function sleepSync(ms) {
|
|
240
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
241
|
+
}
|
|
242
|
+
function isAlreadyExists(error) {
|
|
243
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
244
|
+
}
|
|
245
|
+
function isNotFound(error) {
|
|
246
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
247
|
+
}
|
|
248
|
+
function invokedAsCli() {
|
|
249
|
+
const entry = process.argv[1];
|
|
250
|
+
if (!entry) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
return resolve(fileURLToPath(import.meta.url)) === resolve(entry);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return pathToFileURL(resolve(entry)).href === import.meta.url;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (invokedAsCli()) {
|
|
261
|
+
try {
|
|
262
|
+
runFileBuildLedgerCli(process.argv);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface CreatePullRequestOptions {
|
|
2
|
+
title: string;
|
|
3
|
+
body: string;
|
|
4
|
+
base: string;
|
|
5
|
+
head: string;
|
|
6
|
+
draft?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface PullRequestRef {
|
|
9
|
+
url: string;
|
|
10
|
+
number: number;
|
|
11
|
+
}
|
|
12
|
+
export interface GitHubCli {
|
|
13
|
+
createPullRequest(options: CreatePullRequestOptions): Promise<PullRequestRef>;
|
|
14
|
+
}
|
|
15
|
+
export interface GitOps {
|
|
16
|
+
currentBranch(): string;
|
|
17
|
+
checkoutNewBranch(branchName: string): void;
|
|
18
|
+
checkoutExisting(branchName: string): void;
|
|
19
|
+
add(paths: string[]): void;
|
|
20
|
+
commit(message: string, paths: string[]): void;
|
|
21
|
+
push(branchName: string, setUpstream: boolean): void;
|
|
22
|
+
hasChanges(paths: string[]): boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function createExecGitOps(cwd: string): GitOps;
|
|
25
|
+
export declare function createExecGitHubCli(cwd: string, repo?: string): GitHubCli;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { communityAppsGitHubRepo, githubToken } from "./github-repo.js";
|
|
3
|
+
export function createExecGitOps(cwd) {
|
|
4
|
+
const run = (args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5
|
+
return {
|
|
6
|
+
currentBranch() {
|
|
7
|
+
return run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
8
|
+
},
|
|
9
|
+
checkoutNewBranch(branchName) {
|
|
10
|
+
run(["checkout", "-b", branchName]);
|
|
11
|
+
},
|
|
12
|
+
checkoutExisting(branchName) {
|
|
13
|
+
run(["checkout", branchName]);
|
|
14
|
+
},
|
|
15
|
+
add(paths) {
|
|
16
|
+
if (paths.length === 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
run(["add", "--", ...paths]);
|
|
20
|
+
},
|
|
21
|
+
commit(message, paths) {
|
|
22
|
+
if (paths.length === 0) {
|
|
23
|
+
throw new Error("git commit requires a pathspec");
|
|
24
|
+
}
|
|
25
|
+
run(["commit", "-m", message, "--", ...paths]);
|
|
26
|
+
},
|
|
27
|
+
push(branchName, setUpstream) {
|
|
28
|
+
const args = ["push"];
|
|
29
|
+
if (setUpstream) {
|
|
30
|
+
args.push("-u", "origin", branchName);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
args.push("origin", branchName);
|
|
34
|
+
}
|
|
35
|
+
run(args);
|
|
36
|
+
},
|
|
37
|
+
hasChanges(paths) {
|
|
38
|
+
if (paths.length === 0) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const status = run(["status", "--porcelain", "--", ...paths]);
|
|
42
|
+
return status.length > 0;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function createExecGitHubCli(cwd, repo) {
|
|
47
|
+
const targetRepo = repo ?? communityAppsGitHubRepo();
|
|
48
|
+
return {
|
|
49
|
+
async createPullRequest(options) {
|
|
50
|
+
const token = githubToken();
|
|
51
|
+
const args = [
|
|
52
|
+
"pr",
|
|
53
|
+
"create",
|
|
54
|
+
"--repo",
|
|
55
|
+
targetRepo,
|
|
56
|
+
"--title",
|
|
57
|
+
options.title,
|
|
58
|
+
"--body",
|
|
59
|
+
options.body,
|
|
60
|
+
"--base",
|
|
61
|
+
options.base,
|
|
62
|
+
"--head",
|
|
63
|
+
options.head,
|
|
64
|
+
];
|
|
65
|
+
if (options.draft) {
|
|
66
|
+
args.push("--draft");
|
|
67
|
+
}
|
|
68
|
+
const stdout = execFileSync("gh", args, {
|
|
69
|
+
cwd,
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
72
|
+
env: {
|
|
73
|
+
...process.env,
|
|
74
|
+
...(token ? { GH_TOKEN: token, GITHUB_TOKEN: token } : {}),
|
|
75
|
+
},
|
|
76
|
+
}).trim();
|
|
77
|
+
const url = stdout.split(/\s+/).pop() ?? stdout;
|
|
78
|
+
const match = url.match(/\/pull\/(\d+)\/?$/);
|
|
79
|
+
return {
|
|
80
|
+
url,
|
|
81
|
+
number: match ? Number(match[1]) : 0,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const DEFAULT_GITHUB_OWNER = "synopackageland";
|
|
2
|
+
export declare const COMMUNITY_APPS_GITHUB_REPO = "synopkgland-community-apps";
|
|
3
|
+
export declare const PLATFORM_GITHUB_REPO = "syno-package-land";
|
|
4
|
+
export declare function githubOwner(env?: NodeJS.ProcessEnv): string;
|
|
5
|
+
export declare function communityAppsGitHubRepo(env?: NodeJS.ProcessEnv): string;
|
|
6
|
+
export declare function communityAppsGitHubUrl(env?: NodeJS.ProcessEnv): string;
|
|
7
|
+
export declare function platformGitHubUrl(env?: NodeJS.ProcessEnv): string;
|
|
8
|
+
export declare function githubToken(env?: NodeJS.ProcessEnv): string | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const DEFAULT_GITHUB_OWNER = "synopackageland";
|
|
2
|
+
export const COMMUNITY_APPS_GITHUB_REPO = "synopkgland-community-apps";
|
|
3
|
+
export const PLATFORM_GITHUB_REPO = "syno-package-land";
|
|
4
|
+
export function githubOwner(env = process.env) {
|
|
5
|
+
const fromEnv = env.GITHUB_OWNER?.trim();
|
|
6
|
+
return fromEnv && fromEnv.length > 0 ? fromEnv : DEFAULT_GITHUB_OWNER;
|
|
7
|
+
}
|
|
8
|
+
export function communityAppsGitHubRepo(env = process.env) {
|
|
9
|
+
const repo = env.GITHUB_COMMUNITY_APPS_REPO?.trim() || COMMUNITY_APPS_GITHUB_REPO;
|
|
10
|
+
return `${githubOwner(env)}/${repo}`;
|
|
11
|
+
}
|
|
12
|
+
export function communityAppsGitHubUrl(env = process.env) {
|
|
13
|
+
return `https://github.com/${communityAppsGitHubRepo(env)}`;
|
|
14
|
+
}
|
|
15
|
+
export function platformGitHubUrl(env = process.env) {
|
|
16
|
+
return `https://github.com/${githubOwner(env)}/${PLATFORM_GITHUB_REPO}`;
|
|
17
|
+
}
|
|
18
|
+
export function githubToken(env = process.env) {
|
|
19
|
+
const token = env.GH_TOKEN?.trim() || env.GITHUB_TOKEN?.trim();
|
|
20
|
+
return token && token.length > 0 ? token : undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ProjectRoot } from "../discovery.js";
|
|
2
|
+
import type { GitHubCli, GitOps } from "./github-pr.js";
|
|
3
|
+
export interface PublishProjectOptions {
|
|
4
|
+
project: ProjectRoot;
|
|
5
|
+
communityAppsRoot?: string;
|
|
6
|
+
baseBranch?: string;
|
|
7
|
+
branchPrefix?: string;
|
|
8
|
+
github: GitHubCli;
|
|
9
|
+
git: GitOps;
|
|
10
|
+
now?: () => Date;
|
|
11
|
+
}
|
|
12
|
+
export interface PublishProjectResult {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
code?: string;
|
|
15
|
+
message?: string;
|
|
16
|
+
packageIdentity?: string;
|
|
17
|
+
prUrl?: string;
|
|
18
|
+
prNumber?: number;
|
|
19
|
+
branchName?: string;
|
|
20
|
+
noChanges?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function publishProject(options: PublishProjectOptions): Promise<PublishProjectResult>;
|
|
23
|
+
export { resolveCommunityAppsRoot, communityAppDirectory, listCommunityAppDirectories } from "./community-path.js";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { loadContract } from "../contract/load.js";
|
|
3
|
+
import { validateProject } from "../validate.js";
|
|
4
|
+
import { assertDirectoryMatchesPackage, COMMUNITY_APPS_APPS_DIR, COMMUNITY_APPS_DIR, communityAppDirectory, resolveCommunityAppsRoot, } from "./community-path.js";
|
|
5
|
+
import { listSyncedRelativePaths, syncPackagingDefinition } from "./sync-definition.js";
|
|
6
|
+
function slugifyPackageIdentity(packageIdentity) {
|
|
7
|
+
return packageIdentity.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
8
|
+
}
|
|
9
|
+
function renderPullRequestBody(packageIdentity, appRelativePath, files) {
|
|
10
|
+
return [
|
|
11
|
+
"## Summary",
|
|
12
|
+
"",
|
|
13
|
+
`Publish Packaging Definition for \`${packageIdentity}\`.`,
|
|
14
|
+
"",
|
|
15
|
+
"## Checklist",
|
|
16
|
+
"",
|
|
17
|
+
"- [ ] 變更摘要",
|
|
18
|
+
"- [ ] Image 來源(公開 registry/digest)",
|
|
19
|
+
"- [ ] 是否新增 User File Scope 或 secret",
|
|
20
|
+
"",
|
|
21
|
+
"## Synced paths",
|
|
22
|
+
"",
|
|
23
|
+
`\`${appRelativePath}/\``,
|
|
24
|
+
"",
|
|
25
|
+
files.map((file) => `- \`${file}\``).join("\n"),
|
|
26
|
+
"",
|
|
27
|
+
"_Opened by `synopkgland publish`. PR CI runs validate + preview only; no signing or Package Build allocation._",
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
export async function publishProject(options) {
|
|
31
|
+
const validation = validateProject(options.project, { profile: "listing" });
|
|
32
|
+
if (!validation.ok) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
code: "VALIDATION_FAILED",
|
|
36
|
+
message: validation.errors.map((error) => error.code).join(", "),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const communityRoot = options.communityAppsRoot ?? resolveCommunityAppsRoot(options.project.root);
|
|
40
|
+
if (!communityRoot) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
code: "COMMUNITY_APPS_ROOT_MISSING",
|
|
44
|
+
message: "找不到 synopkgland-community-apps/apps。請在 monorepo 根目錄執行,或設定 SYNOPKGLAND_COMMUNITY_APPS_ROOT。",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const contract = loadContract(options.project.contractPath).contract;
|
|
48
|
+
const packageIdentity = contract.package;
|
|
49
|
+
if (typeof packageIdentity !== "string" || packageIdentity.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
code: "PACKAGE_IDENTITY_MISSING",
|
|
53
|
+
message: "App Contract 缺少有效的 package 欄位。",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const targetDir = communityAppDirectory(communityRoot, packageIdentity);
|
|
57
|
+
try {
|
|
58
|
+
assertDirectoryMatchesPackage(packageIdentity, targetDir);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
code: "PACKAGE_DIRECTORY_MISMATCH",
|
|
64
|
+
message: error instanceof Error ? error.message : String(error),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (resolve(options.project.root) === resolve(targetDir)) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
code: "PUBLISH_SOURCE_IS_COMMUNITY_APP",
|
|
71
|
+
message: "publish 的專案根目錄不能是 Community Apps 目標目錄。請對開發專案執行,不要對 synopkgland-community-apps/apps/<package> 本身執行。",
|
|
72
|
+
packageIdentity,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
syncPackagingDefinition(options.project, targetDir);
|
|
76
|
+
const appRelativePath = join(COMMUNITY_APPS_DIR, COMMUNITY_APPS_APPS_DIR, packageIdentity);
|
|
77
|
+
const syncedFiles = listSyncedRelativePaths(targetDir);
|
|
78
|
+
const baseBranch = options.baseBranch ?? "master";
|
|
79
|
+
const branchPrefix = options.branchPrefix ?? "publish";
|
|
80
|
+
const stamp = (options.now ?? (() => new Date()))().toISOString().replace(/[:.]/g, "-");
|
|
81
|
+
const branchName = `${branchPrefix}/${slugifyPackageIdentity(packageIdentity)}-${stamp}`;
|
|
82
|
+
if (!options.git.hasChanges([appRelativePath])) {
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
noChanges: true,
|
|
86
|
+
packageIdentity,
|
|
87
|
+
message: "Community Apps 定義已是最新,未建立 PR。",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const previousBranch = options.git.currentBranch();
|
|
91
|
+
try {
|
|
92
|
+
options.git.checkoutNewBranch(branchName);
|
|
93
|
+
options.git.add([appRelativePath]);
|
|
94
|
+
options.git.commit(`Publish ${packageIdentity} packaging definition`, [appRelativePath]);
|
|
95
|
+
options.git.push(branchName, true);
|
|
96
|
+
const pr = await options.github.createPullRequest({
|
|
97
|
+
title: `Publish ${packageIdentity}`,
|
|
98
|
+
body: renderPullRequestBody(packageIdentity, appRelativePath, syncedFiles),
|
|
99
|
+
base: baseBranch,
|
|
100
|
+
head: branchName,
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
packageIdentity,
|
|
105
|
+
prUrl: pr.url,
|
|
106
|
+
prNumber: pr.number,
|
|
107
|
+
branchName,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
code: "PUBLISH_PR_FAILED",
|
|
114
|
+
message: error instanceof Error ? error.message : String(error),
|
|
115
|
+
packageIdentity,
|
|
116
|
+
branchName,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
try {
|
|
121
|
+
options.git.checkoutExisting(previousBranch);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Best effort restore; caller may be in a temp repo during tests.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export { resolveCommunityAppsRoot, communityAppDirectory, listCommunityAppDirectories } from "./community-path.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProjectRoot } from "../discovery.js";
|
|
2
|
+
export declare function assertDistinctSyncPaths(sourceRoot: string, targetDir: string): void;
|
|
3
|
+
export declare function syncPackagingDefinition(source: ProjectRoot, targetDir: string): void;
|
|
4
|
+
export declare function listSyncedRelativePaths(targetDir: string): string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
const PACKAGING_DEFINITION_FILES = new Set(["synology-app.yaml", "compose.yaml", "README.md", "icon.png"]);
|
|
4
|
+
const PACKAGING_DEFINITION_DIRS = new Set(["html"]);
|
|
5
|
+
export function assertDistinctSyncPaths(sourceRoot, targetDir) {
|
|
6
|
+
if (resolve(sourceRoot) === resolve(targetDir)) {
|
|
7
|
+
throw new Error("publish 的專案根目錄不能是 Community Apps 目標目錄。請對開發專案執行,不要對 synopkgland-community-apps/apps/<package> 本身執行。");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function syncPackagingDefinition(source, targetDir) {
|
|
11
|
+
assertDistinctSyncPaths(source.root, targetDir);
|
|
12
|
+
if (existsSync(targetDir)) {
|
|
13
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
mkdirSync(targetDir, { recursive: true });
|
|
16
|
+
for (const name of PACKAGING_DEFINITION_FILES) {
|
|
17
|
+
const from = join(source.root, name);
|
|
18
|
+
if (existsSync(from) && statSync(from).isFile()) {
|
|
19
|
+
cpSync(from, join(targetDir, name));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const name of PACKAGING_DEFINITION_DIRS) {
|
|
23
|
+
const from = join(source.root, name);
|
|
24
|
+
if (existsSync(from) && statSync(from).isDirectory()) {
|
|
25
|
+
cpSync(from, join(targetDir, name), { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function listSyncedRelativePaths(targetDir) {
|
|
30
|
+
const paths = [];
|
|
31
|
+
const walk = (current, prefix) => {
|
|
32
|
+
if (!existsSync(current)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
36
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
37
|
+
const absolute = join(current, entry.name);
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
walk(absolute, rel);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
paths.push(rel);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
walk(targetDir, "");
|
|
47
|
+
return paths.sort();
|
|
48
|
+
}
|
package/dist/skill.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SkillEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
file: string;
|
|
4
|
+
when: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveSkillsRoot(): string;
|
|
7
|
+
export declare function listSkills(): SkillEntry[];
|
|
8
|
+
export declare function skillPath(id: string): string;
|
|
9
|
+
export declare function readSkill(id: string): string;
|
|
10
|
+
export declare function formatSkillList(): string;
|