@synopackageland/cli 0.32.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 +2 -1
- package/README.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/error-registry.js +1 -1
- package/dist/init-upstream.js +6 -6
- package/dist/package-metadata.d.ts +8 -1
- package/dist/package-metadata.js +28 -1
- package/dist/upstream-attribution.js +4 -3
- package/package.json +1 -1
- package/schema/package-metadata.schema.json +32 -3
- package/schema/synology-app.schema.json +32 -3
- package/skills/SKILL.md +1 -1
- package/skills/reference/compose.md +1 -1
- package/skills/reference/upstream-fork.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.32.
|
|
3
|
+
## 0.32.1 (2026-10-27)
|
|
4
4
|
|
|
5
5
|
- Attribute upstream Apps to their original project/team or owner and the packaging publisher as distributor, with generic skill guidance and a release checklist
|
|
6
6
|
- Prefill upstream-fork attribution from the upstream repository and publisher options or environment configuration; use a bounded public GitHub owner lookup and explicit TODOs offline
|
|
7
7
|
- Warn when upstream attribution is incomplete or the maintainer equals the distributor; warnings appear in check JSON and keep exit status successful
|
|
8
8
|
- Keep templates 0.31.x and its SDK defaults; CLI minor releases can ship independently of templates
|
|
9
|
+
- Write upstream provenance as the catalog-compatible `{name, url, license?}` object; normalize HTTPS URL shorthand when building SPKs
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ CLI-only releases can differ from the SDK patch. Compose import and SDK defaults
|
|
|
74
74
|
read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
|
|
75
75
|
|
|
76
76
|
`init` and `add` read their templates from the locally installed
|
|
77
|
-
`@synopackageland/templates`. CLI 0.32.
|
|
77
|
+
`@synopackageland/templates`. CLI 0.32.1 depends on `~0.31.0`: npm installation or
|
|
78
78
|
update can select the latest templates patch in the 0.31 minor line. Templates
|
|
79
79
|
patches need only a templates release; update the npm installation to receive them.
|
|
80
80
|
The CLI never fetches templates at runtime, so offline use works after installation;
|
package/dist/cli.js
CHANGED
|
@@ -473,7 +473,7 @@ export async function runCli(argv) {
|
|
|
473
473
|
const targetDir = resolve(dir);
|
|
474
474
|
const upstream = options.upstreamFork ? upstreamRepository(targetDir, options.upstream) : undefined;
|
|
475
475
|
if (options.upstream && !upstream)
|
|
476
|
-
throw failure("INIT_UPSTREAM_INVALID", "Supply an
|
|
476
|
+
throw failure("INIT_UPSTREAM_INVALID", "Supply an HTTPS repository URL without credentials");
|
|
477
477
|
if (!options.upstreamFork && (options.upstream || options.upstreamMaintainer || options.publisher || options.publisherUrl))
|
|
478
478
|
throw failure("INIT_OPTIONS_UNSUPPORTED", "Attribution options require --upstream-fork");
|
|
479
479
|
initFromTemplate(options.template, targetDir, {
|
package/dist/error-registry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
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
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
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." },
|
|
7
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." },
|
package/dist/init-upstream.js
CHANGED
|
@@ -3,16 +3,16 @@ 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 } from './package-metadata.js';
|
|
6
|
+
import { safeMetadataUrl, upstreamMetadata } from './package-metadata.js';
|
|
7
7
|
import { parseImageReference } from './images/pin.js';
|
|
8
8
|
export function validateForkRecipe(template, options) {
|
|
9
|
-
if (options.upstream && !safeMetadataUrl(options.upstream))
|
|
10
|
-
throw new Error('[INIT_UPSTREAM_INVALID] Supply an
|
|
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
11
|
if (options.publisherUrl && !safeMetadataUrl(options.publisherUrl))
|
|
12
12
|
throw new Error('[INIT_PUBLISHER_INVALID] Supply an http(s) publisher URL without credentials.');
|
|
13
13
|
for (const value of [options.publisher, options.upstreamMaintainer]) {
|
|
14
|
-
if (value !== undefined && (!value.trim() || /[\x00-\x1f\x7f"\\]/.test(value) || value.length >
|
|
15
|
-
throw new Error('[INIT_PUBLISHER_INVALID] Attribution names must be plain, non-empty single-line text.');
|
|
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
16
|
}
|
|
17
17
|
if (template === 'native-go' && options.upstreamImage)
|
|
18
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.');
|
|
@@ -40,7 +40,7 @@ export function writeForkScaffold(packaging, template, options) {
|
|
|
40
40
|
doc.set('maintainer', options.upstreamMaintainer ?? 'TODO: original upstream project or owner');
|
|
41
41
|
doc.setIn(['metadata', 'distributor'], options.publisher ?? 'TODO: your publisher name');
|
|
42
42
|
if (options.upstream) {
|
|
43
|
-
doc.setIn(['metadata', 'upstream'], options.upstream);
|
|
43
|
+
doc.setIn(['metadata', 'upstream'], upstreamMetadata(options.upstream));
|
|
44
44
|
doc.setIn(['metadata', 'maintainer_url'], options.upstream);
|
|
45
45
|
}
|
|
46
46
|
if (options.publisherUrl)
|
|
@@ -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,7 +14,7 @@ export interface PackageMetadata {
|
|
|
7
14
|
support_email?: string;
|
|
8
15
|
homepage?: string;
|
|
9
16
|
source_url?: string;
|
|
10
|
-
upstream?:
|
|
17
|
+
upstream?: UpstreamMetadata;
|
|
11
18
|
issues_url?: string;
|
|
12
19
|
license?: string;
|
|
13
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;
|
|
@@ -40,6 +63,10 @@ export function validateMetadata(raw) {
|
|
|
40
63
|
for (const [k, v] of Object.entries(raw)) {
|
|
41
64
|
if (!allowed.includes(k))
|
|
42
65
|
throw new Error("unknown metadata");
|
|
66
|
+
if (k === "upstream") {
|
|
67
|
+
upstreamMetadata(v);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
43
70
|
if (k === "screenshots") {
|
|
44
71
|
if (!Array.isArray(v) ||
|
|
45
72
|
v.length > MAX_SCREENSHOTS ||
|
|
@@ -79,7 +106,7 @@ export function validateMetadata(raw) {
|
|
|
79
106
|
}
|
|
80
107
|
if (JSON.stringify(raw).length > 32000)
|
|
81
108
|
throw new Error("metadata too large");
|
|
82
|
-
return raw;
|
|
109
|
+
return { ...raw, ...("upstream" in raw ? { upstream: upstreamMetadata(raw.upstream) } : {}) };
|
|
83
110
|
}
|
|
84
111
|
export function metadataInfoFields(metadata) {
|
|
85
112
|
if (!metadata)
|
|
@@ -11,7 +11,7 @@ export function upstreamRepository(root, explicit) {
|
|
|
11
11
|
if (!raw)
|
|
12
12
|
return undefined;
|
|
13
13
|
const url = raw.replace(/^git@github\.com:/, 'https://github.com/').replace(/\.git$/, '');
|
|
14
|
-
return safeMetadataUrl(url) ? url : undefined;
|
|
14
|
+
return safeMetadataUrl(url) && url.startsWith("https:") ? url : undefined;
|
|
15
15
|
}
|
|
16
16
|
/** Best effort, bounded and public: offline/rate-limited lookups retain an explicit TODO. */
|
|
17
17
|
export async function upstreamMaintainer(url, fetcher = fetch) {
|
|
@@ -28,7 +28,7 @@ export async function upstreamMaintainer(url, fetcher = fetch) {
|
|
|
28
28
|
return undefined;
|
|
29
29
|
const owner = await response.json();
|
|
30
30
|
const name = typeof owner.name === 'string' && owner.name.trim() ? owner.name.trim() : owner.login;
|
|
31
|
-
return typeof name === 'string' && name.length <=
|
|
31
|
+
return typeof name === 'string' && name.length <= 80 && !/[\x00-\x1f\x7f"\\]/.test(name) ? name : undefined;
|
|
32
32
|
}
|
|
33
33
|
catch {
|
|
34
34
|
return undefined;
|
|
@@ -45,7 +45,8 @@ export function upstreamAttributionWarnings(root) {
|
|
|
45
45
|
const todo = (v) => typeof v !== 'string' || !v.trim() || /^TODO\b/i.test(v.trim());
|
|
46
46
|
if (!todo(raw.maintainer) && !todo(m.distributor) &&
|
|
47
47
|
raw.maintainer.trim().toLowerCase() !== m.distributor.trim().toLowerCase() &&
|
|
48
|
-
!todo(m.maintainer_url) && !todo(m.distributor_url)
|
|
48
|
+
!todo(m.maintainer_url) && !todo(m.distributor_url) &&
|
|
49
|
+
!(typeof m.upstream === "object" && m.upstream && todo(m.upstream.name)))
|
|
49
50
|
return [];
|
|
50
51
|
return [{
|
|
51
52
|
code: 'UPSTREAM_ATTRIBUTION', severity: 'warning',
|
package/package.json
CHANGED
|
@@ -98,9 +98,38 @@
|
|
|
98
98
|
}
|
|
99
99
|
},
|
|
100
100
|
"upstream": {
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
]
|
|
104
133
|
}
|
|
105
134
|
}
|
|
106
135
|
}
|
|
@@ -189,9 +189,38 @@
|
|
|
189
189
|
"additionalProperties": false,
|
|
190
190
|
"properties": {
|
|
191
191
|
"upstream": {
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
]
|
|
195
224
|
},
|
|
196
225
|
"changelog": {
|
|
197
226
|
"oneOf": [
|
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.32.
|
|
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
|
|
@@ -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 defaults from locally installed templates `~0.31.0` with CLI 0.32.
|
|
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
|
|
|
@@ -27,7 +27,8 @@ Pass `--upstream <repo-url>` (or retain the `upstream` Git remote), your verifie
|
|
|
27
27
|
GitHub owner profile with a bounded lookup, using its display name then login.
|
|
28
28
|
Offline or unsupported hosts leave an explicit maintainer TODO. Missing publisher
|
|
29
29
|
configuration leaves a distributor TODO. Resolve these manually before release.
|
|
30
|
-
`metadata.upstream` records the
|
|
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
|
|
31
32
|
missing attribution or a maintainer that equals the distributor. The fork layout
|
|
32
33
|
also enables this warning without `metadata.upstream`.
|
|
33
34
|
|