@synopackageland/cli 0.31.0 → 0.32.1
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/CHANGELOG.md +6 -799
- package/README.md +11 -3
- package/dist/cli.js +18 -1
- package/dist/error-registry.js +3 -0
- package/dist/init-upstream.d.ts +4 -0
- package/dist/init-upstream.js +19 -0
- package/dist/init.d.ts +4 -0
- package/dist/package-metadata.d.ts +8 -0
- package/dist/package-metadata.js +30 -1
- package/dist/upstream-attribution.d.ts +7 -0
- package/dist/upstream-attribution.js +57 -0
- package/dist/validate.js +2 -1
- package/package.json +1 -1
- package/schema/package-metadata.schema.json +34 -0
- package/schema/synology-app.schema.json +34 -0
- package/skills/SKILL.md +9 -1
- package/skills/reference/compose.md +1 -1
- package/skills/reference/upstream-fork.md +39 -3
package/README.md
CHANGED
|
@@ -29,6 +29,14 @@ synopkgland status --host my-nas --json
|
|
|
29
29
|
`deploy --dry-run` prints the deployment plan without connecting to a NAS. You can also install
|
|
30
30
|
the SPK with Package Center's Manual Install.
|
|
31
31
|
|
|
32
|
+
When packaging someone else's open-source App, name its original project/team
|
|
33
|
+
(or owner display name, then login) as maintainer and yourself as distributor.
|
|
34
|
+
Use `init --upstream-fork --upstream <repo-url> --publisher <name> --publisher-url <url>`;
|
|
35
|
+
`--upstream-maintainer` records a verified project/team name. Publisher defaults
|
|
36
|
+
come from `SYNOPKGLAND_PUBLISHER` / `SYNOPKGLAND_PUBLISHER_URL`. Offline lookups
|
|
37
|
+
leave TODOs; `check --json` reports attribution warnings without failing.
|
|
38
|
+
See the [upstream scaffold and release checklist](skills/reference/upstream-fork.md).
|
|
39
|
+
|
|
32
40
|
## Templates
|
|
33
41
|
|
|
34
42
|
| Template | Use it for | Manifest |
|
|
@@ -62,12 +70,12 @@ alongside npm lockfiles. Do not disable checksum verification for public SDK
|
|
|
62
70
|
packages. Private mirrors can use explicitly configured Go authentication and
|
|
63
71
|
checksum settings; a failed fetch reports `SDK_DOWNLOAD_FAILED`.
|
|
64
72
|
|
|
65
|
-
CLI-only
|
|
73
|
+
CLI-only releases can differ from the SDK patch. Compose import and SDK defaults
|
|
66
74
|
read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
|
|
67
75
|
|
|
68
76
|
`init` and `add` read their templates from the locally installed
|
|
69
|
-
`@synopackageland/templates`. CLI 0.
|
|
70
|
-
update can select the latest templates patch in the 0.
|
|
77
|
+
`@synopackageland/templates`. CLI 0.32.1 depends on `~0.31.0`: npm installation or
|
|
78
|
+
update can select the latest templates patch in the 0.31 minor line. Templates
|
|
71
79
|
patches need only a templates release; update the npm installation to receive them.
|
|
72
80
|
The CLI never fetches templates at runtime, so offline use works after installation;
|
|
73
81
|
commit a lockfile and use `npm ci` to reproduce a chosen dependency tree.
|
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { upstreamRepository, upstreamMaintainer } from './upstream-attribution.js';
|
|
1
2
|
import { Command, CommanderError } from "commander";
|
|
2
3
|
import { existsSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
@@ -324,6 +325,8 @@ export async function runCli(argv) {
|
|
|
324
325
|
throw failure(validation.errors[0].code, formatErrors(validation.errors), validation.errors[0].suggestion);
|
|
325
326
|
const standalone = standaloneRoot(rootOf(command));
|
|
326
327
|
if (standalone) {
|
|
328
|
+
if (validation.warnings?.length)
|
|
329
|
+
console.error(formatErrors(validation.warnings));
|
|
327
330
|
const loaded = isNativeLayout(standalone)
|
|
328
331
|
? loadNativeLayout(standalone)
|
|
329
332
|
: loadStandaloneManifest(standalone);
|
|
@@ -348,6 +351,7 @@ export async function runCli(argv) {
|
|
|
348
351
|
: undefined;
|
|
349
352
|
emit("check", {
|
|
350
353
|
package: loaded.manifest.package,
|
|
354
|
+
warnings: validation.warnings ?? [],
|
|
351
355
|
...("installSettings" in loaded.manifest && loaded.manifest.installSettings ? { installSettings: loaded.manifest.installSettings } : {}),
|
|
352
356
|
...("identityGateway" in loaded.manifest && gatewayAdmissionReport(loaded.manifest.identityGateway, loaded.manifest.entry) ? { identityGateway: gatewayAdmissionReport(loaded.manifest.identityGateway, loaded.manifest.entry) } : {}),
|
|
353
357
|
capabilities: "capabilities" in loaded.manifest
|
|
@@ -442,6 +446,10 @@ export async function runCli(argv) {
|
|
|
442
446
|
.option("--package <id>", "INFO.package (defaults to the directory name)")
|
|
443
447
|
.option("--name <display>", "Display name")
|
|
444
448
|
.option("--upstream-image <repository>", "compose fork: official image repository; upgrade pins its matching tag and index digest")
|
|
449
|
+
.option("--upstream <url>", "Original upstream repository URL (defaults to the upstream Git remote)")
|
|
450
|
+
.option("--upstream-maintainer <name>", "Verified original project/team name; otherwise resolve the GitHub owner")
|
|
451
|
+
.option("--publisher <name>", "Your distributor name (or SYNOPKGLAND_PUBLISHER)")
|
|
452
|
+
.option("--publisher-url <url>", "Your distributor URL (or SYNOPKGLAND_PUBLISHER_URL)")
|
|
445
453
|
.option("--upstream-fork", "Create only synology/ inside an upstream source fork (compose or native-go)")
|
|
446
454
|
.option("--health-path <path>", "compose: actual readiness endpoint; redirects are not followed")
|
|
447
455
|
.option("--health-status <status>", "compose: expected 2xx readiness status (no default)", Number)
|
|
@@ -455,7 +463,7 @@ export async function runCli(argv) {
|
|
|
455
463
|
.option("--min-dsm <version>", `Minimum DSM version (native templates and binary default to ${DEFAULT_MIN_DSM}, compose to ${CONTAINER_MIN_DSM})`)
|
|
456
464
|
.argument("[dir]", "Target directory", ".")
|
|
457
465
|
.addHelpText("after", "\nNative projects: native-go, native-python, native-node, native-rust and binary use dsm-package.yaml schema 2.\nCompose projects: compose uses dsm-package.yaml schema 2; --from imports compose.yaml.\nQuick start: synopkgland init -t native-go --with files,jobs,login my-app\n cd my-app && synopkgland build\n")
|
|
458
|
-
.action((dir, options, command) => {
|
|
466
|
+
.action(async (dir, options, command) => {
|
|
459
467
|
if (command.parent?.opts().project)
|
|
460
468
|
throw failure("CLI_OPTION_INVALID", "--project does not apply to init.");
|
|
461
469
|
if (!options.template)
|
|
@@ -463,7 +471,16 @@ export async function runCli(argv) {
|
|
|
463
471
|
if (!isTemplateName(options.template))
|
|
464
472
|
throw failure("INIT_TEMPLATE_UNKNOWN", `Unknown template: ${options.template}`);
|
|
465
473
|
const targetDir = resolve(dir);
|
|
474
|
+
const upstream = options.upstreamFork ? upstreamRepository(targetDir, options.upstream) : undefined;
|
|
475
|
+
if (options.upstream && !upstream)
|
|
476
|
+
throw failure("INIT_UPSTREAM_INVALID", "Supply an HTTPS repository URL without credentials");
|
|
477
|
+
if (!options.upstreamFork && (options.upstream || options.upstreamMaintainer || options.publisher || options.publisherUrl))
|
|
478
|
+
throw failure("INIT_OPTIONS_UNSUPPORTED", "Attribution options require --upstream-fork");
|
|
466
479
|
initFromTemplate(options.template, targetDir, {
|
|
480
|
+
upstream,
|
|
481
|
+
upstreamMaintainer: options.upstreamMaintainer ?? (options.upstreamFork ? await upstreamMaintainer(upstream) : undefined),
|
|
482
|
+
publisher: options.publisher ?? process.env.SYNOPKGLAND_PUBLISHER,
|
|
483
|
+
publisherUrl: options.publisherUrl ?? process.env.SYNOPKGLAND_PUBLISHER_URL,
|
|
467
484
|
upstreamFork: options.upstreamFork,
|
|
468
485
|
upstreamImage: options.upstreamImage,
|
|
469
486
|
healthPath: options.healthPath,
|
package/dist/error-registry.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { COMPOSE_ERRORS } from "./standalone/compose-errors.js";
|
|
2
2
|
export const ERROR_REGISTRY = {
|
|
3
3
|
...COMPOSE_ERRORS,
|
|
4
|
+
INIT_UPSTREAM_INVALID: { code: "INIT_UPSTREAM_INVALID", description: "The upstream repository URL is invalid.", suggestion: "Supply --upstream with an HTTPS repository URL without credentials." },
|
|
5
|
+
INIT_PUBLISHER_INVALID: { code: "INIT_PUBLISHER_INVALID", description: "The attribution name or publisher URL is invalid.", suggestion: "Use plain single-line project/publisher names and an http(s) publisher URL without credentials." },
|
|
6
|
+
UPSTREAM_ATTRIBUTION: { code: "UPSTREAM_ATTRIBUTION", description: "Upstream App attribution is incomplete or names the distributor as maintainer.", suggestion: "Name the original project/team or owner as maintainer; set metadata.maintainer_url to the upstream repository and metadata.distributor/distributor_url to your publisher; resolve TODOs before release." },
|
|
4
7
|
INSTALL_SETTINGS_INVALID: { "code": "INSTALL_SETTINGS_INVALID", "description": "The installation setting declaration is invalid.", "suggestion": "Declare 1–32 lowercase identifier keys in install.settings, each with type https-origin, a plain label and an explicit boolean required; omit defaults and values." },
|
|
5
8
|
SDK_INSTALL_SETTINGS_UNSUPPORTED: { "code": "SDK_INSTALL_SETTINGS_UNSUPPORTED", "description": "The selected SDK does not provide native installation settings.", "suggestion": "Pin SDK 0.24.0 or a matching supported release with installsettings/cmd/control, then rebuild with the matching CLI." },
|
|
6
9
|
SDK_INSTALL_SETTINGS_BUILD_FAILED: { "code": "SDK_INSTALL_SETTINGS_BUILD_FAILED", "description": "The SDK installation settings controller could not be built.", "suggestion": "Install the pinned Go toolchain, inspect the compiler diagnostics and rebuild with a complete matching SDK release." },
|
package/dist/init-upstream.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export interface ForkRecipeOptions {
|
|
2
2
|
upstreamImage?: string;
|
|
3
3
|
from?: string;
|
|
4
|
+
upstream?: string;
|
|
5
|
+
upstreamMaintainer?: string;
|
|
6
|
+
publisher?: string;
|
|
7
|
+
publisherUrl?: string;
|
|
4
8
|
}
|
|
5
9
|
export declare function validateForkRecipe(template: 'compose' | 'native-go', options: ForkRecipeOptions): void;
|
|
6
10
|
/** CLI-owned packaging recipe; upstream files and history stay untouched. */
|
package/dist/init-upstream.js
CHANGED
|
@@ -3,8 +3,17 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { parseDocument } from 'yaml';
|
|
5
5
|
import { API_PERMISSIONS_FILE } from './standalone/api-permissions.js';
|
|
6
|
+
import { safeMetadataUrl, upstreamMetadata } from './package-metadata.js';
|
|
6
7
|
import { parseImageReference } from './images/pin.js';
|
|
7
8
|
export function validateForkRecipe(template, options) {
|
|
9
|
+
if (options.upstream && (!safeMetadataUrl(options.upstream) || !options.upstream.startsWith("https:")))
|
|
10
|
+
throw new Error('[INIT_UPSTREAM_INVALID] Supply an HTTPS upstream repository URL without credentials.');
|
|
11
|
+
if (options.publisherUrl && !safeMetadataUrl(options.publisherUrl))
|
|
12
|
+
throw new Error('[INIT_PUBLISHER_INVALID] Supply an http(s) publisher URL without credentials.');
|
|
13
|
+
for (const value of [options.publisher, options.upstreamMaintainer]) {
|
|
14
|
+
if (value !== undefined && (!value.trim() || /[\x00-\x1f\x7f"\\]/.test(value) || value.length > 80))
|
|
15
|
+
throw new Error('[INIT_PUBLISHER_INVALID] Attribution names must be plain, non-empty single-line text of at most 80 characters.');
|
|
16
|
+
}
|
|
8
17
|
if (template === 'native-go' && options.upstreamImage)
|
|
9
18
|
throw new Error('[INIT_OPTIONS_UNSUPPORTED] Native forks stage official release binaries or use a source build hook; image options apply only to compose.');
|
|
10
19
|
if (template !== 'compose')
|
|
@@ -28,6 +37,14 @@ export function writeForkScaffold(packaging, template, options) {
|
|
|
28
37
|
? { kind: 'official-image', repository: options.upstreamImage, service, imageTag: 'tag' }
|
|
29
38
|
: { kind: 'native-source' };
|
|
30
39
|
writeFileSync(join(packaging, 'upstream-recipe.json'), JSON.stringify(recipe, null, 2) + '\n');
|
|
40
|
+
doc.set('maintainer', options.upstreamMaintainer ?? 'TODO: original upstream project or owner');
|
|
41
|
+
doc.setIn(['metadata', 'distributor'], options.publisher ?? 'TODO: your publisher name');
|
|
42
|
+
if (options.upstream) {
|
|
43
|
+
doc.setIn(['metadata', 'upstream'], upstreamMetadata(options.upstream));
|
|
44
|
+
doc.setIn(['metadata', 'maintainer_url'], options.upstream);
|
|
45
|
+
}
|
|
46
|
+
if (options.publisherUrl)
|
|
47
|
+
doc.setIn(['metadata', 'distributor_url'], options.publisherUrl);
|
|
31
48
|
doc.set('entry', 'admin-only');
|
|
32
49
|
doc.delete('ctl');
|
|
33
50
|
if (official) {
|
|
@@ -53,6 +70,8 @@ export function writeForkScaffold(packaging, template, options) {
|
|
|
53
70
|
const guidance = `
|
|
54
71
|
## Integrating an existing open-source App
|
|
55
72
|
|
|
73
|
+
If you package someone else's open-source project, set maintainer to the original project/owner (prefer the project's own team/org name, else the owner's display name, else login), metadata.maintainer_url to the upstream repo, and yourself as metadata.distributor/metadata.distributor_url. Resolve all attribution TODOs before release; run check and review its warnings. Only your own Apps use your publisher as maintainer.
|
|
74
|
+
|
|
56
75
|
Keep the upstream fork/history and remote upstream. All wrapper-owned files belong in synology/; never edit upstream files or treat the App as an SDK component. Read synopkgland skill show upstream-fork.
|
|
57
76
|
|
|
58
77
|
Leave ctl absent from the manifest by default. Ask whether the user wants AI control; add ctl only when explicitly requested. If requested, read synopkgland skill show ctl and produce ctl-plan covering existing UI actions and public API endpoints, including common reads and writes. Status-only commands do not meet completion. Wrap existing native APIs without changing upstream code or inventing features. Long operations must return through existing asynchronous APIs without waiting for completion; report synchronous-only long APIs as blockers for discussion.
|
package/dist/init.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface InitOptions {
|
|
|
18
18
|
from?: string;
|
|
19
19
|
upstreamFork?: boolean;
|
|
20
20
|
upstreamImage?: string;
|
|
21
|
+
upstream?: string;
|
|
22
|
+
upstreamMaintainer?: string;
|
|
23
|
+
publisher?: string;
|
|
24
|
+
publisherUrl?: string;
|
|
21
25
|
healthPath?: string;
|
|
22
26
|
healthStatus?: number;
|
|
23
27
|
/** native-go: `install.sharedFolder` (select from the NAS, or create one). key defaults to "shared", permission to "rw". */
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/** Plain text, or a map from a BCP-47 tag (en, zh-TW, ...) to plain text. */
|
|
2
2
|
export type LocalizedText = string | Record<string, string>;
|
|
3
|
+
export interface UpstreamMetadata {
|
|
4
|
+
name: string;
|
|
5
|
+
url: string;
|
|
6
|
+
license?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Normalize the URL shorthand to the catalog's existing structured contract. */
|
|
9
|
+
export declare function upstreamMetadata(raw: unknown): UpstreamMetadata;
|
|
3
10
|
export interface PackageMetadata {
|
|
4
11
|
long_description?: LocalizedText;
|
|
5
12
|
changelog?: LocalizedText;
|
|
@@ -7,6 +14,7 @@ export interface PackageMetadata {
|
|
|
7
14
|
support_email?: string;
|
|
8
15
|
homepage?: string;
|
|
9
16
|
source_url?: string;
|
|
17
|
+
upstream?: UpstreamMetadata;
|
|
10
18
|
issues_url?: string;
|
|
11
19
|
license?: string;
|
|
12
20
|
maintainer_url?: string;
|
package/dist/package-metadata.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/** Normalize the URL shorthand to the catalog's existing structured contract. */
|
|
2
|
+
export function upstreamMetadata(raw) {
|
|
3
|
+
const value = typeof raw === "string" ? { name: upstreamName(raw), url: raw } : raw;
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
throw new Error("invalid upstream");
|
|
6
|
+
const v = value;
|
|
7
|
+
const text = (t) => typeof t === "string" && !!t.trim() && t.length <= 80 && !/[\x00-\x1f\x7f]/.test(t);
|
|
8
|
+
if (Object.keys(v).some(k => !["name", "url", "license"].includes(k)) ||
|
|
9
|
+
!text(v.name) || !safeMetadataUrl(v.url) || !v.url.startsWith("https:") ||
|
|
10
|
+
(v.license !== undefined && !text(v.license)))
|
|
11
|
+
throw new Error("invalid upstream");
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function upstreamName(url) {
|
|
15
|
+
try {
|
|
16
|
+
const u = new URL(url);
|
|
17
|
+
const name = decodeURIComponent(u.pathname.split("/").filter(Boolean).at(-1) ?? u.hostname).replace(/\.git$/, "");
|
|
18
|
+
return name.trim() && name.length <= 80 && !/[\x00-\x1f\x7f]/.test(name) ? name : "TODO: original upstream project";
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return "TODO: original upstream project";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
1
24
|
export function safeMetadataUrl(value, mail = false) {
|
|
2
25
|
if (typeof value !== "string" || value.length > 2048)
|
|
3
26
|
return false;
|
|
@@ -27,6 +50,7 @@ export function validateMetadata(raw) {
|
|
|
27
50
|
"support_email",
|
|
28
51
|
"homepage",
|
|
29
52
|
"source_url",
|
|
53
|
+
"upstream",
|
|
30
54
|
"issues_url",
|
|
31
55
|
"license",
|
|
32
56
|
"maintainer_url",
|
|
@@ -39,6 +63,10 @@ export function validateMetadata(raw) {
|
|
|
39
63
|
for (const [k, v] of Object.entries(raw)) {
|
|
40
64
|
if (!allowed.includes(k))
|
|
41
65
|
throw new Error("unknown metadata");
|
|
66
|
+
if (k === "upstream") {
|
|
67
|
+
upstreamMetadata(v);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
42
70
|
if (k === "screenshots") {
|
|
43
71
|
if (!Array.isArray(v) ||
|
|
44
72
|
v.length > MAX_SCREENSHOTS ||
|
|
@@ -66,6 +94,7 @@ export function validateMetadata(raw) {
|
|
|
66
94
|
if ([
|
|
67
95
|
"homepage",
|
|
68
96
|
"source_url",
|
|
97
|
+
"upstream",
|
|
69
98
|
"issues_url",
|
|
70
99
|
"maintainer_url",
|
|
71
100
|
"distributor_url",
|
|
@@ -77,7 +106,7 @@ export function validateMetadata(raw) {
|
|
|
77
106
|
}
|
|
78
107
|
if (JSON.stringify(raw).length > 32000)
|
|
79
108
|
throw new Error("metadata too large");
|
|
80
|
-
return raw;
|
|
109
|
+
return { ...raw, ...("upstream" in raw ? { upstream: upstreamMetadata(raw.upstream) } : {}) };
|
|
81
110
|
}
|
|
82
111
|
export function metadataInfoFields(metadata) {
|
|
83
112
|
if (!metadata)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ValidationError } from './types.js';
|
|
2
|
+
/** A repository URL is provenance, never a credential-bearing transport URL. */
|
|
3
|
+
export declare function upstreamRepository(root: string, explicit?: string): string | undefined;
|
|
4
|
+
/** Best effort, bounded and public: offline/rate-limited lookups retain an explicit TODO. */
|
|
5
|
+
export declare function upstreamMaintainer(url?: string, fetcher?: typeof fetch): Promise<string | undefined>;
|
|
6
|
+
/** Advice only: attribution never changes manifest validity or the command exit status. */
|
|
7
|
+
export declare function upstreamAttributionWarnings(root: string): ValidationError[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { safeMetadataUrl } from './package-metadata.js';
|
|
6
|
+
/** A repository URL is provenance, never a credential-bearing transport URL. */
|
|
7
|
+
export function upstreamRepository(root, explicit) {
|
|
8
|
+
const raw = explicit ?? spawnSync('git', ['remote', 'get-url', 'upstream'], {
|
|
9
|
+
cwd: root, encoding: 'utf8', timeout: 1000,
|
|
10
|
+
}).stdout?.trim();
|
|
11
|
+
if (!raw)
|
|
12
|
+
return undefined;
|
|
13
|
+
const url = raw.replace(/^git@github\.com:/, 'https://github.com/').replace(/\.git$/, '');
|
|
14
|
+
return safeMetadataUrl(url) && url.startsWith("https:") ? url : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Best effort, bounded and public: offline/rate-limited lookups retain an explicit TODO. */
|
|
17
|
+
export async function upstreamMaintainer(url, fetcher = fetch) {
|
|
18
|
+
if (!url)
|
|
19
|
+
return undefined;
|
|
20
|
+
const match = /^https:\/\/github\.com\/([\w-]+)\/[^/?#]+\/?$/.exec(url);
|
|
21
|
+
if (!match)
|
|
22
|
+
return undefined;
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetcher(`https://api.github.com/users/${match[1]}`, {
|
|
25
|
+
headers: { Accept: 'application/vnd.github+json' }, signal: AbortSignal.timeout(3000),
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok)
|
|
28
|
+
return undefined;
|
|
29
|
+
const owner = await response.json();
|
|
30
|
+
const name = typeof owner.name === 'string' && owner.name.trim() ? owner.name.trim() : owner.login;
|
|
31
|
+
return typeof name === 'string' && name.length <= 80 && !/[\x00-\x1f\x7f"\\]/.test(name) ? name : undefined;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Advice only: attribution never changes manifest validity or the command exit status. */
|
|
38
|
+
export function upstreamAttributionWarnings(root) {
|
|
39
|
+
const raw = parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8'));
|
|
40
|
+
const m = raw?.metadata ?? {};
|
|
41
|
+
const fork = existsSync(join(root, 'upstream-recipe.json')) ||
|
|
42
|
+
(basename(root) === 'synology' && existsSync(join(root, 'upgrade.sh')));
|
|
43
|
+
if (!m.upstream && !fork)
|
|
44
|
+
return [];
|
|
45
|
+
const todo = (v) => typeof v !== 'string' || !v.trim() || /^TODO\b/i.test(v.trim());
|
|
46
|
+
if (!todo(raw.maintainer) && !todo(m.distributor) &&
|
|
47
|
+
raw.maintainer.trim().toLowerCase() !== m.distributor.trim().toLowerCase() &&
|
|
48
|
+
!todo(m.maintainer_url) && !todo(m.distributor_url) &&
|
|
49
|
+
!(typeof m.upstream === "object" && m.upstream && todo(m.upstream.name)))
|
|
50
|
+
return [];
|
|
51
|
+
return [{
|
|
52
|
+
code: 'UPSTREAM_ATTRIBUTION', severity: 'warning',
|
|
53
|
+
span: { file: 'dsm-package.yaml', line: 1, column: 1 },
|
|
54
|
+
message: 'Upstream App attribution is incomplete or names the distributor as maintainer',
|
|
55
|
+
suggestion: "Set maintainer to the original project/team, else the owner's display name or login; set metadata.maintainer_url to the upstream repository and metadata.distributor/distributor_url to your publisher. Resolve attribution TODOs before release",
|
|
56
|
+
}];
|
|
57
|
+
}
|
package/dist/validate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { upstreamAttributionWarnings } from './upstream-attribution.js';
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { getErrorCode } from "./error-registry.js";
|
|
@@ -36,6 +37,6 @@ export function validateProject(project, _options = {}) {
|
|
|
36
37
|
"Correct the schema-2 declaration; see the SDK Compose support matrix.",
|
|
37
38
|
})),
|
|
38
39
|
}
|
|
39
|
-
: { ok: true, errors: [] };
|
|
40
|
+
: { ok: true, errors: [], warnings: upstreamAttributionWarnings(project.root) };
|
|
40
41
|
}
|
|
41
42
|
export { findProjectRoot } from "./discovery.js";
|
package/package.json
CHANGED
|
@@ -96,6 +96,40 @@
|
|
|
96
96
|
"pattern": "^https://",
|
|
97
97
|
"maxLength": 2048
|
|
98
98
|
}
|
|
99
|
+
},
|
|
100
|
+
"upstream": {
|
|
101
|
+
"oneOf": [
|
|
102
|
+
{
|
|
103
|
+
"type": "string",
|
|
104
|
+
"maxLength": 2048,
|
|
105
|
+
"pattern": "^https://"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"type": "object",
|
|
109
|
+
"additionalProperties": false,
|
|
110
|
+
"required": [
|
|
111
|
+
"name",
|
|
112
|
+
"url"
|
|
113
|
+
],
|
|
114
|
+
"properties": {
|
|
115
|
+
"name": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"minLength": 1,
|
|
118
|
+
"maxLength": 80
|
|
119
|
+
},
|
|
120
|
+
"url": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"maxLength": 2048,
|
|
123
|
+
"pattern": "^https://"
|
|
124
|
+
},
|
|
125
|
+
"license": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"minLength": 1,
|
|
128
|
+
"maxLength": 80
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
]
|
|
99
133
|
}
|
|
100
134
|
}
|
|
101
135
|
}
|
|
@@ -188,6 +188,40 @@
|
|
|
188
188
|
"type": "object",
|
|
189
189
|
"additionalProperties": false,
|
|
190
190
|
"properties": {
|
|
191
|
+
"upstream": {
|
|
192
|
+
"oneOf": [
|
|
193
|
+
{
|
|
194
|
+
"type": "string",
|
|
195
|
+
"maxLength": 2048,
|
|
196
|
+
"pattern": "^https://"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"type": "object",
|
|
200
|
+
"additionalProperties": false,
|
|
201
|
+
"required": [
|
|
202
|
+
"name",
|
|
203
|
+
"url"
|
|
204
|
+
],
|
|
205
|
+
"properties": {
|
|
206
|
+
"name": {
|
|
207
|
+
"type": "string",
|
|
208
|
+
"minLength": 1,
|
|
209
|
+
"maxLength": 80
|
|
210
|
+
},
|
|
211
|
+
"url": {
|
|
212
|
+
"type": "string",
|
|
213
|
+
"maxLength": 2048,
|
|
214
|
+
"pattern": "^https://"
|
|
215
|
+
},
|
|
216
|
+
"license": {
|
|
217
|
+
"type": "string",
|
|
218
|
+
"minLength": 1,
|
|
219
|
+
"maxLength": 80
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
},
|
|
191
225
|
"changelog": {
|
|
192
226
|
"oneOf": [
|
|
193
227
|
{
|
package/skills/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Initialize, extend, check, build, inspect, and deploy Synology appl
|
|
|
5
5
|
|
|
6
6
|
# Independent Synology SPKs
|
|
7
7
|
|
|
8
|
-
CLI 0.
|
|
8
|
+
CLI 0.32.1 installs templates through `~0.31.0`. A templates patch needs only a
|
|
9
9
|
templates release; npm install/update selects a patch in that minor line. The CLI
|
|
10
10
|
uses the locally installed templates and reads their SDK pin for defaults; it
|
|
11
11
|
never downloads templates at runtime. Preserve the lockfile and use `npm ci` for
|
|
@@ -22,6 +22,14 @@ Use this guide to initialize, build and inspect own applications or upstream OSS
|
|
|
22
22
|
|
|
23
23
|
For an existing open-source App, first read [reference/upstream-fork.md](reference/upstream-fork.md): upstream fork/history + untouched upstream files + all packaging in `synology/`, official container images pinned by tag/index digest, native official release binaries pinned by SHA-256 or a source build hook, and a single tag-merge upgrade command. Use `init --upstream-fork`; never rewrite the App as a component. Integrated upstream Apps require direct DSM administrator entry without a separate App password or an OIDC/SSO server: supported trusted headers first, disabling native login second; the shipped OpenList pattern uses the existing SDK 0.17.3 token adapter (`mode: token`, `access: dsm-admins`, `entry: admin-only`) with server-side Authorization injection. All DSM admins share one upstream admin; token reset requires package restart; WebDAV retains its native credentials.
|
|
24
24
|
|
|
25
|
+
**If you package someone else's open-source project, set `maintainer` to the
|
|
26
|
+
original project/owner (prefer the project's own team/org name, else the owner's
|
|
27
|
+
display name, else login), `metadata.maintainer_url` to the upstream repository,
|
|
28
|
+
and yourself as `metadata.distributor` / `metadata.distributor_url`.** Only your
|
|
29
|
+
own Apps use your publisher as maintainer. Pass upstream and publisher attribution
|
|
30
|
+
to `init --upstream-fork`; resolve TODOs and review `check` warnings before release.
|
|
31
|
+
See the scaffold steps and release checklist in [reference/upstream-fork.md](reference/upstream-fork.md).
|
|
32
|
+
|
|
25
33
|
For upstream OSS packaging, leave `ctl` absent from the manifest by default. Ask whether the user wants AI control; implement it only when explicitly requested. Read [reference/ctl.md](reference/ctl.md) for the required `ctl-plan`, UI/API coverage checklist and asynchronous-operation rules. Own Apps retain the native template's ctl defaults, with commands covering their main features; status-only coverage is incomplete.
|
|
26
34
|
|
|
27
35
|
Start with `synopkgland init -t native-go`, `synopkgland init -t binary`, or `synopkgland init -t compose`. Read reference/create.md or reference/wrap.md, then reference/deploy.md. Native CGI session checks belong to the package. For wrapping upstream OSS with DSM accounts, read [reference/identity-gateway.md](reference/identity-gateway.md) first. For all Compose work, read [reference/compose.md](reference/compose.md): schema-2 upstream files, generated override, SDK planner, fixed package identity and current DSM blockers. Containers do not inherit a signed-in user's File Station ACL. Existing Shared Folder mounts are blocked until official protected ACL/project control is verified.
|
|
@@ -247,7 +247,7 @@ declared availability readiness, LAN isolation/egress, protected grant lifecycle
|
|
|
247
247
|
ACLs, two-user visibility and clean install/upgrade/reboot/stop/uninstall before
|
|
248
248
|
calling the package supported on a NAS. No NAS is accessed by the build gate.
|
|
249
249
|
|
|
250
|
-
The unified Compose contract uses SDK
|
|
250
|
+
The unified Compose contract uses SDK defaults from locally installed templates `~0.31.0` with CLI 0.32.1. Templates patches can ship independently; npm install/update resolves them, and SDK defaults come from the installed templates. SDK minor upgrades do not require a CLI release when they declare supported executor manifest format 1; the resolved SDK owns the release mapping and verified payload digests. The CLI does not fetch templates at runtime. The legacy packaging route and bridge publish mode are removed.
|
|
251
251
|
|
|
252
252
|
Primary override files, and each included override set, must share a directory. Cross-directory override bases are rejected before loader I/O. Included implicit .env files are confined, inventoried and preserved; symlinks are rejected.
|
|
253
253
|
|
|
@@ -11,6 +11,31 @@ under `synology/`, such as DSM install wizards and gateway pages; leave upstream
|
|
|
11
11
|
UI/locale strings and release notes unchanged, and exclude upstream source from
|
|
12
12
|
any future punctuation checker.
|
|
13
13
|
|
|
14
|
+
## Maintainer and distributor
|
|
15
|
+
|
|
16
|
+
If you package someone else's open-source project, set `maintainer` to the original
|
|
17
|
+
project/owner (prefer the project's own team/org name, else the owner's display
|
|
18
|
+
name, else login), `metadata.maintainer_url` to the upstream repository, and
|
|
19
|
+
**yourself** as `metadata.distributor` / `metadata.distributor_url`. Only your own
|
|
20
|
+
Apps use your publisher as maintainer. Syno Package Land is the distributor only
|
|
21
|
+
when it distributes the App; this rule applies to every third-party developer.
|
|
22
|
+
|
|
23
|
+
Pass `--upstream <repo-url>` (or retain the `upstream` Git remote), your verified
|
|
24
|
+
`--upstream-maintainer <project-team>` when known, and `--publisher <name>` /
|
|
25
|
+
`--publisher-url <url>` to `init --upstream-fork`. Publisher defaults come from
|
|
26
|
+
`SYNOPKGLAND_PUBLISHER` / `SYNOPKGLAND_PUBLISHER_URL`. The CLI tries the public
|
|
27
|
+
GitHub owner profile with a bounded lookup, using its display name then login.
|
|
28
|
+
Offline or unsupported hosts leave an explicit maintainer TODO. Missing publisher
|
|
29
|
+
configuration leaves a distributor TODO. Resolve these manually before release.
|
|
30
|
+
`metadata.upstream: {name, url}` records the original project name and HTTPS repository;
|
|
31
|
+
URL shorthand is normalized to that object in SPK metadata; `check` warns, without failing, for
|
|
32
|
+
missing attribution or a maintainer that equals the distributor. The fork layout
|
|
33
|
+
also enables this warning without `metadata.upstream`.
|
|
34
|
+
|
|
35
|
+
These metadata fields become standard SPK INFO fields; upstream source stays
|
|
36
|
+
unchanged. Catalog attribution may change before rebuilding; installed INFO
|
|
37
|
+
changes only after an SPK upgrade.
|
|
38
|
+
|
|
14
39
|
## Decide whether to expose AI control
|
|
15
40
|
|
|
16
41
|
Ask whether the user needs AI control unless they have already explicitly requested it.
|
|
@@ -37,11 +62,12 @@ git clone <your-fork-url> app
|
|
|
37
62
|
cd app
|
|
38
63
|
git remote add upstream <upstream-url>
|
|
39
64
|
synopkgland init -t compose --upstream-fork --package MyApp \
|
|
40
|
-
--upstream
|
|
65
|
+
--upstream https://github.com/owner/app --publisher "Your team" \
|
|
66
|
+
--publisher-url https://example.com --upstream-image owner/app --health-path /actual-readiness --health-status 200 .
|
|
41
67
|
```
|
|
42
68
|
|
|
43
69
|
Replace `owner/app` with the verified official repository. `init` does not guess
|
|
44
|
-
|
|
70
|
+
the official image publisher. Only attribution uses a best-effort GitHub profile lookup. Configure `synology/upstream-recipe.json`:
|
|
45
71
|
`repository`, the actual Compose `service`, and `imageTag: tag` (exact upstream
|
|
46
72
|
tag) or `version` (strip the leading `v` only if the official image uses that
|
|
47
73
|
spelling). Adjust the App's port, storage and DSM login adapter before committing.
|
|
@@ -78,7 +104,9 @@ workaround with the framework ownership mechanism once available and verified.
|
|
|
78
104
|
## Native: official release binaries or a source build hook
|
|
79
105
|
|
|
80
106
|
```sh
|
|
81
|
-
synopkgland init -t native-go --ui plain --upstream-fork --package MyApp
|
|
107
|
+
synopkgland init -t native-go --ui plain --upstream-fork --package MyApp \
|
|
108
|
+
--upstream https://github.com/owner/app --publisher "Your team" \
|
|
109
|
+
--publisher-url https://example.com .
|
|
82
110
|
```
|
|
83
111
|
|
|
84
112
|
Native Apps can stage official release binaries pinned by **SHA-256**, or compile
|
|
@@ -180,3 +208,11 @@ returns 503 even when the gateway is down. Availability is not identity attestat
|
|
|
180
208
|
Declare only Container Manager-capable `arches`/INFO platforms; exclude unsupported
|
|
181
209
|
NAS architectures and review models in each family. Actual missing-package wizard
|
|
182
210
|
behavior and DSM's dependency not-ready behavior remain device acceptance items.
|
|
211
|
+
|
|
212
|
+
## Release checklist
|
|
213
|
+
|
|
214
|
+
- Verify the original project/team or owner is the maintainer, its repository is
|
|
215
|
+
`metadata.maintainer_url` / `metadata.upstream`, and your publisher is the distributor
|
|
216
|
+
- Resolve attribution TODOs and review all `check --json` warnings
|
|
217
|
+
- Inspect the built SPK INFO attribution, preserve notices/source correspondence,
|
|
218
|
+
and verify the selected upstream release before publishing
|