@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 CHANGED
@@ -1,8 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.32.0 (2026-10-27)
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.0 depends on `~0.31.0`: npm installation or
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 http(s) repository URL without credentials");
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, {
@@ -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 http(s) repository URL without credentials." },
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." },
@@ -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 http(s) upstream repository URL without credentials.');
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 > 200))
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?: string;
17
+ upstream?: UpstreamMetadata;
11
18
  issues_url?: string;
12
19
  license?: string;
13
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;
@@ -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 <= 200 && !/[\x00-\x1f\x7f"\\]/.test(name) ? name : undefined;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -98,9 +98,38 @@
98
98
  }
99
99
  },
100
100
  "upstream": {
101
- "type": "string",
102
- "maxLength": 2048,
103
- "pattern": "^https?://"
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
- "type": "string",
193
- "maxLength": 2048,
194
- "pattern": "^https?://"
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.0 installs templates through `~0.31.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
@@ -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.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
 
@@ -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 repository; `check` warns, without failing, for
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