@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/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 patch releases can differ from the SDK patch. Compose import and SDK defaults
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.31.0 depends on `~0.31.0`: npm installation or
70
- update can select the latest templates patch in the 0.29 minor line. Templates
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,
@@ -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." },
@@ -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. */
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.31.0",
3
+ "version": "0.32.1",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.27.x installs templates through `~0.27.0`. A templates patch needs only a
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 0.27.x with CLI 0.27.x and locally installed templates from `~0.27.0`. 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.
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-image owner/app --health-path /actual-readiness --health-status 200 .
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
- its publisher or make network requests. Configure `synology/upstream-recipe.json`:
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