@synopackageland/cli 0.41.13 → 0.42.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 +26 -1
- package/README.md +22 -9
- package/dist/cli.js +2 -0
- package/dist/ctl/cli.d.ts +5 -0
- package/dist/ctl/cli.js +12 -5
- package/dist/ctl/input.d.ts +5 -1
- package/dist/ctl/input.js +28 -14
- package/dist/ctl/service.d.ts +17 -5
- package/dist/ctl/service.js +33 -22
- package/dist/error-registry.js +1 -1
- package/dist/host/package-commands.d.ts +15 -0
- package/dist/host/package-commands.js +15 -4
- package/dist/runtime/sdk-source.d.ts +1 -1
- package/dist/runtime/sdk-source.js +1 -1
- package/docs/ctl-external.md +16 -0
- package/package.json +2 -2
- package/skills/SKILL.md +1 -1
- package/skills/reference/ctl.md +22 -0
- package/skills/reference/install.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.42.1 (2026-11-01)
|
|
4
|
+
|
|
5
|
+
- Add `--version` and `-V` to report the installed CLI version.
|
|
6
|
+
- Register the installed-App approval diagnostic as `CTL_APP_NEEDS_APPROVAL`
|
|
7
|
+
with the App-scoped approve command, replacing the retired error entry.
|
|
8
|
+
|
|
9
|
+
## 0.42.0 (2026-11-01)
|
|
10
|
+
|
|
11
|
+
- Require templates ~0.37.0 so `init` and `add` scaffold with UI 0.19.1 and
|
|
12
|
+
SDK/App 0.41.1 (executor/v0.39.0). CLI 0.41.x stays on the templates 0.35
|
|
13
|
+
minor line.
|
|
14
|
+
- Pin CLI-owned runtime defaults (daemon controllers, Compose planning and
|
|
15
|
+
installer context) to SDK 0.41.1, matching the templates. Explicit App SDK
|
|
16
|
+
declarations still take precedence.
|
|
17
|
+
- Skip installation when the requested package version is already installed,
|
|
18
|
+
including DSM build suffixes; report `action: "none"` in plans and avoid
|
|
19
|
+
downloads, uploads and reinstalling on confirmed runs.
|
|
20
|
+
- Accept repeated ctl flags for arrays of scalar values, validate their item
|
|
21
|
+
schemas and expose repeatable flags in command help and examples. Preserve
|
|
22
|
+
sensitive-value redaction and JSON input for complex parameters.
|
|
23
|
+
- Return `CTL_APP_NEEDS_APPROVAL` (exit 4) with an App-scoped
|
|
24
|
+
`ctl list --app <package> --approve-installed` hint when an installed App,
|
|
25
|
+
called by package ID or a cached alias, awaits first, new-version or
|
|
26
|
+
changed-description approval. Cached aliases identify upgraded packages but
|
|
27
|
+
never authorize execution; `CTL_APP_UNKNOWN` suggestions apply only to names
|
|
28
|
+
matching no installed package or known alias.
|
|
4
29
|
|
|
5
30
|
## 0.41.13
|
|
6
31
|
|
package/README.md
CHANGED
|
@@ -89,8 +89,8 @@ alongside npm lockfiles. Do not disable checksum verification for public SDK
|
|
|
89
89
|
packages. Private mirrors can use explicitly configured Go authentication and
|
|
90
90
|
checksum settings; a failed fetch reports `SDK_DOWNLOAD_FAILED`.
|
|
91
91
|
|
|
92
|
-
The CLI pins SDK 0.41.
|
|
93
|
-
context. Explicit App SDK declarations take precedence. Templates
|
|
92
|
+
The CLI pins SDK 0.41.1 for its daemon controllers, Compose planning and installer
|
|
93
|
+
context. Explicit App SDK declarations take precedence. Templates use ~0.37.0
|
|
94
94
|
and retain their own scaffold SDK pins; none declare daemon ingress.
|
|
95
95
|
Every App method must be declared in the permissions table. Dynamic names and
|
|
96
96
|
custom HTTP dispatchers are rejected. With SDK 0.33.0, use fixed
|
|
@@ -101,8 +101,8 @@ unimported development commands are excluded. Use
|
|
|
101
101
|
authentication. See [App entry and registration](docs/app-entry.md).
|
|
102
102
|
|
|
103
103
|
`init` and `add` read their templates from the locally installed
|
|
104
|
-
`@synopackageland/templates`. CLI 0.
|
|
105
|
-
update can select the latest templates patch in the 0.
|
|
104
|
+
`@synopackageland/templates`. CLI 0.42.0 depends on `~0.37.0`: npm installation or
|
|
105
|
+
update can select the latest templates patch in the 0.37 minor line. Templates
|
|
106
106
|
patches need only a templates release; update the npm installation to receive them.
|
|
107
107
|
The CLI never fetches templates at runtime, so offline use works after installation;
|
|
108
108
|
commit a lockfile and use `npm ci` to reproduce a chosen dependency tree.
|
|
@@ -537,7 +537,7 @@ for `ctl` calls; App command/job/transfer deadlines retain their existing defaul
|
|
|
537
537
|
| 1 | Unexpected internal error (`CTL_INTERNAL`), or an uncaught CLI exception. |
|
|
538
538
|
| 2 | Authentication/session failure: `CTL_NOT_AUTHENTICATED`, `CTL_AUTH_FAILED`, `CTL_CREDENTIAL_MISMATCH`, or unavailable OS credential storage. |
|
|
539
539
|
| 3 | Invalid parameters, schema validation or unknown names: `CTL_PARAMS_INVALID`, `CTL_ARGUMENT_INVALID`, `CTL_APP_UNKNOWN`, `CTL_COMMAND_UNKNOWN`. |
|
|
540
|
-
| 4 |
|
|
540
|
+
| 4 | Approval/trust or permission refusal: `CTL_APP_NEEDS_APPROVAL`, `CTL_PERMISSION_DENIED`, `CTL_INVENTORY_DENIED`. |
|
|
541
541
|
| 5 | App/contract failure that fails the command: `CTL_APP_ERROR`, `CTL_RESPONSE_INVALID`, `CTL_JOB_FAILED`. |
|
|
542
542
|
| 6 | Network/HTTP failure or uncertain completion: `CTL_NETWORK`, `CTL_HTTP_ERROR`, `CTL_JOB_TIMEOUT`, `CTL_LOGOUT_FAILED`. |
|
|
543
543
|
| 7 | Confirmation required (`CTL_CONFIRM_REQUIRED`): a write/destructive command or host removal without confirmation; no business operation runs. |
|
|
@@ -547,13 +547,26 @@ For example, `synopkgland ctl studio drafts create --host nas --json` without
|
|
|
547
547
|
`--yes` returns `ok:false` with `CTL_CONFIRM_REQUIRED` and exits 7 once discovery
|
|
548
548
|
and parameter validation succeed. Missing sessions fail earlier with exit 2.
|
|
549
549
|
Discovery catches per-App description errors and excludes those Apps from its
|
|
550
|
-
results. For example, a changed pinned description (`
|
|
550
|
+
results. For example, a changed pinned description (`CTL_APP_NEEDS_APPROVAL`)
|
|
551
551
|
or an invalid descriptor (`CTL_DESCRIPTOR_INVALID`) appears in
|
|
552
552
|
`ctl list --host nas --refresh --json` under `data.diagnostics`; the listing still
|
|
553
553
|
returns `ok:true` and exits 0, even if no Apps remain. These diagnostic codes do
|
|
554
|
-
not become process exit codes 4 or 5
|
|
555
|
-
|
|
556
|
-
|
|
554
|
+
not become process exit codes 4 or 5 for a listing. Executing or requesting help
|
|
555
|
+
for an installed App by package ID or a known cached alias instead returns its
|
|
556
|
+
specific error; no business operation runs. A first approval, new version, or
|
|
557
|
+
changed description returns `CTL_APP_NEEDS_APPROVAL` and exit 4, for example
|
|
558
|
+
`SynoReclaim 1.2.0 needs approval`, with the exact App-scoped hint:
|
|
559
|
+
|
|
560
|
+
```sh
|
|
561
|
+
synopkgland ctl list --app SynoReclaim --approve-installed --host <profile>
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Obtain the user's authorization before approving. Previously approved aliases
|
|
565
|
+
identify upgraded installed packages without granting permission to execute.
|
|
566
|
+
Use the package ID for first approval when no alias has been discovered yet.
|
|
567
|
+
Only a name matching no installed package ID or known alias returns
|
|
568
|
+
`CTL_APP_UNKNOWN` with “Did you mean …” suggestions. Other description failures,
|
|
569
|
+
such as `CTL_DESCRIPTOR_INVALID`, remain specific errors (exit 5).
|
|
557
570
|
|
|
558
571
|
`host list/show` can also succeed while reporting `authenticated:false` and
|
|
559
572
|
exit 0. Check discovery `apps`/`diagnostics` and session fields as well as the exit
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { nasFlags, withNasOptions } from './nas/options.js';
|
|
2
2
|
import { upstreamRepository, upstreamMaintainer } from './upstream-attribution.js';
|
|
3
3
|
import { Command, CommanderError } from "commander";
|
|
4
|
+
import { CLI_VERSION } from "./cli-version.js";
|
|
4
5
|
import { existsSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
6
7
|
import { basename, resolve, dirname, join } from "node:path";
|
|
@@ -322,6 +323,7 @@ async function runCliWithin(argv) {
|
|
|
322
323
|
const program = new Command()
|
|
323
324
|
.name("synopkgland")
|
|
324
325
|
.description("Syno Package Land Developer Kit CLI")
|
|
326
|
+
.version(CLI_VERSION)
|
|
325
327
|
.option("--project <dir>", "App project root")
|
|
326
328
|
.option("--strict-tls", "Verify NAS CA, hostname, validity and recorded certificate")
|
|
327
329
|
.option("--ca <file>", "Verify NAS HTTPS with a PEM CA file")
|
package/dist/ctl/cli.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface CtlIO {
|
|
|
12
12
|
export declare function listView(host: string, apps: Descriptor[]): {
|
|
13
13
|
commands: {
|
|
14
14
|
parameters: string | Record<string, any>;
|
|
15
|
+
flags: {
|
|
16
|
+
flag: string;
|
|
17
|
+
valueType: string;
|
|
18
|
+
repeatable: boolean;
|
|
19
|
+
}[];
|
|
15
20
|
examples: string[];
|
|
16
21
|
summary: string;
|
|
17
22
|
cli: string[];
|
package/dist/ctl/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Writable } from 'node:stream';
|
|
|
6
6
|
import { CtlError, errorData, ctlError } from './errors.js';
|
|
7
7
|
import { CtlStore } from './store.js';
|
|
8
8
|
import { CtlClient, boundedMap } from './service.js';
|
|
9
|
-
import { selectHosts,
|
|
9
|
+
import { selectHosts, selectOperation, flagName, flagValueType, RESERVED_FLAGS } from './input.js';
|
|
10
10
|
import { canonicalOrigin, CTL_TIMEOUT_MS, createRequest } from './transport.js';
|
|
11
11
|
import { loginDsm, logoutDsm } from './dsm-auth.js';
|
|
12
12
|
import { publicOperation } from './redaction.js';
|
|
@@ -51,16 +51,23 @@ function take(args, flag, value = false) {
|
|
|
51
51
|
return answer;
|
|
52
52
|
}
|
|
53
53
|
function q(value) { return "'" + value.replaceAll("'", "'\\''") + "'"; }
|
|
54
|
+
function typedFlags(op) {
|
|
55
|
+
const fields = Object.entries(op.paramsSchema?.properties ?? {}), names = fields.map(([key]) => flagName(key));
|
|
56
|
+
if (names.some(name => RESERVED_FLAGS.has(name)) || new Set(names).size !== names.length)
|
|
57
|
+
return [];
|
|
58
|
+
return fields.flatMap(([key, schema]) => /^[A-Za-z][A-Za-z0-9_]*$/.test(key) && flagValueType(schema) ? [{ flag: '--' + flagName(key), valueType: flagValueType(schema), repeatable: schema.type === 'array' }] : []);
|
|
59
|
+
}
|
|
54
60
|
function example(host, app, op) {
|
|
55
61
|
const base = `synopkgland ctl ${app.payload.aliases[0] ?? app.payload.package.id} ${op.cli.join(' ')} --host ${host}`;
|
|
62
|
+
const flags = typedFlags(op);
|
|
56
63
|
return (op.examples?.length ? op.examples : [{}]).map(params => {
|
|
57
|
-
const simple = op.paramsSchema && Object.entries(params).every(([key, value]) => ['string', 'number', 'boolean'].includes(typeof
|
|
64
|
+
const simple = op.paramsSchema && Object.entries(params).every(([key, value]) => { const flag = flags.find(f => f.flag === '--' + flagName(key)); return flag && (flag.repeatable ? Array.isArray(value) && value.length > 0 && value.every(v => ['string', 'number', 'boolean'].includes(typeof v)) : ['string', 'number', 'boolean'].includes(typeof value)); });
|
|
58
65
|
if (simple)
|
|
59
|
-
return base + Object.entries(params).
|
|
66
|
+
return base + Object.entries(params).flatMap(([key, value]) => (Array.isArray(value) ? value : [value]).map(item => ' --' + flagName(key) + ' ' + q(String(item)))).join('') + (op.effect === 'read' ? '' : ' --yes');
|
|
60
67
|
return `printf '%s' ${q(JSON.stringify(params))} | ${base} --input -` + (op.effect === 'read' ? '' : ' --yes');
|
|
61
68
|
});
|
|
62
69
|
}
|
|
63
|
-
export function listView(host, apps) { return apps.map(app => ({ host, package: app.payload.package, aliases: app.payload.aliases, protocolVersion: app.version, developedBy: app.payload.provenance.developedBy, provenance: 'self-declared', hash: app.sha256, ...(app.payload.external ? { external: app.payload.external } : {}), commands: Object.entries(app.payload.commands).map(([id, original]) => { const op = publicOperation(original); return { id, ...op, parameters: op.paramsSchema ?? 'Undescribed parameters; use --input', examples: example(host, app, op) }; }) })); }
|
|
70
|
+
export function listView(host, apps) { return apps.map(app => ({ host, package: app.payload.package, aliases: app.payload.aliases, protocolVersion: app.version, developedBy: app.payload.provenance.developedBy, provenance: 'self-declared', hash: app.sha256, ...(app.payload.external ? { external: app.payload.external } : {}), commands: Object.entries(app.payload.commands).map(([id, original]) => { const op = publicOperation(original); return { id, ...op, parameters: op.paramsSchema ?? 'Undescribed parameters; use --input', flags: typedFlags(op), examples: example(host, app, op) }; }) })); }
|
|
64
71
|
export async function runCtl(argv, io = {}) {
|
|
65
72
|
if (argv[0] === 'external')
|
|
66
73
|
return runExternal(argv.slice(1), io);
|
|
@@ -237,7 +244,7 @@ async function runCtlWithin(argv, io = {}) {
|
|
|
237
244
|
if (args[0] === 'help' || help) {
|
|
238
245
|
const nameArg = args[0] === 'help' ? args[1] : args[0];
|
|
239
246
|
const data = await ctl.discover(discoverOptions);
|
|
240
|
-
const app = selectApp(data
|
|
247
|
+
const app = ctl.selectApp(data, nameArg ?? '');
|
|
241
248
|
const path = args[0] === 'help' ? args.slice(2) : args.slice(1);
|
|
242
249
|
const view = listView(name, [app])[0];
|
|
243
250
|
return { host: name, ok: true, data: path.length ? { ...view, commands: [view.commands.find(c => c.id === selectOperation(app, path).id)] } : view };
|
package/dist/ctl/input.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { type Descriptor, type Operation } from './descriptor.js';
|
|
2
2
|
export declare function selectHosts(names: string[], selected?: string, all?: boolean): string[];
|
|
3
|
-
export declare function selectApp
|
|
3
|
+
export declare function selectApp<T extends {
|
|
4
|
+
id: string;
|
|
5
|
+
aliases: string[];
|
|
6
|
+
}>(apps: T[], name: string): T;
|
|
4
7
|
export declare function selectOperation(app: Descriptor, path: string[]): {
|
|
5
8
|
id: string;
|
|
6
9
|
operation: Operation;
|
|
7
10
|
};
|
|
8
11
|
export declare const RESERVED_FLAGS: Set<string>;
|
|
9
12
|
export declare function flagName(key: string): string;
|
|
13
|
+
export declare function flagValueType(schema: any): string | undefined;
|
|
10
14
|
export declare function operationInput(op: Operation, args: string[], stdin?: string, helpHint?: string): Record<string, unknown>;
|
|
11
15
|
export declare function requestPlan(app: Descriptor, id: string, params: Record<string, unknown>): {
|
|
12
16
|
package: string;
|
package/dist/ctl/input.js
CHANGED
|
@@ -21,12 +21,15 @@ export function selectHosts(names, selected, all = false) {
|
|
|
21
21
|
return [...new Set(hosts)];
|
|
22
22
|
}
|
|
23
23
|
export function selectApp(apps, name) {
|
|
24
|
-
const
|
|
24
|
+
const exact = apps.find(app => app.id.toLowerCase() === name.toLowerCase());
|
|
25
|
+
if (exact)
|
|
26
|
+
return exact;
|
|
27
|
+
const found = apps.filter(app => app.aliases.some(alias => alias.toLowerCase() === name.toLowerCase()));
|
|
25
28
|
if (found.length > 1)
|
|
26
|
-
throw new CtlError('CTL_APP_AMBIGUOUS', 'Alias matches multiple packages.', 'Use an exact DSM package ID.', 3, found.map(
|
|
29
|
+
throw new CtlError('CTL_APP_AMBIGUOUS', 'Alias matches multiple packages.', 'Use an exact DSM package ID.', 3, found.map(app => app.id));
|
|
27
30
|
if (found.length === 1)
|
|
28
31
|
return found[0];
|
|
29
|
-
return choose(name, apps.flatMap(
|
|
32
|
+
return choose(name, apps.flatMap(app => [app.id, ...app.aliases]), 'app');
|
|
30
33
|
}
|
|
31
34
|
export function selectOperation(app, path) {
|
|
32
35
|
const found = Object.entries(app.payload.commands).find(([, op]) => op.cli.join(' ') === path.join(' '));
|
|
@@ -36,6 +39,10 @@ export function selectOperation(app, path) {
|
|
|
36
39
|
}
|
|
37
40
|
export const RESERVED_FLAGS = new Set(['ca', 'strict-tls', 'host', 'all-hosts', 'json', 'app', 'refresh', 'yes', 'dry-run', 'approve-installed', 'help', 'input', 'wait', 'no-wait', 'timeout']);
|
|
38
41
|
export function flagName(key) { return key.replace(/[A-Z]/g, c => '-' + c.toLowerCase()).replaceAll('_', '-'); }
|
|
42
|
+
export function flagValueType(schema) {
|
|
43
|
+
const type = schema?.type === 'array' ? schema.items?.type : schema?.type;
|
|
44
|
+
return ['string', 'integer', 'number', 'boolean'].includes(type) ? type : undefined;
|
|
45
|
+
}
|
|
39
46
|
export function operationInput(op, args, stdin, helpHint) {
|
|
40
47
|
const fail = (message, allowed) => { throw new CtlError('CTL_PARAMS_INVALID', message, helpHint ?? ('run: synopkgland ctl <package> ' + op.cli.join(' ') + ' --help'), 3, allowed); };
|
|
41
48
|
const inputIndex = args.indexOf('--input');
|
|
@@ -78,35 +85,42 @@ export function operationInput(op, args, stdin, helpHint) {
|
|
|
78
85
|
const field = fields.get(flag);
|
|
79
86
|
if (!field)
|
|
80
87
|
return fail('Unknown parameter flag.', [...fields.keys()]);
|
|
81
|
-
const [key, schema] = field;
|
|
82
|
-
if (Object.hasOwn(params, key))
|
|
88
|
+
const [key, schema] = field, repeatable = schema.type === 'array', type = flagValueType(schema);
|
|
89
|
+
if (Object.hasOwn(params, key) && !repeatable)
|
|
83
90
|
return fail(`Duplicate parameter flag ${flag}.`);
|
|
84
|
-
if (!
|
|
91
|
+
if (!type)
|
|
85
92
|
return fail(`Complex parameter ${flag}: use --input.`);
|
|
86
93
|
let value = split > 0 ? args[i].slice(split + 1) : undefined;
|
|
87
|
-
if (value === undefined &&
|
|
94
|
+
if (value === undefined && type === 'boolean' && (!args[i + 1] || args[i + 1].startsWith('--')))
|
|
88
95
|
value = 'true';
|
|
96
|
+
else if (value === undefined && (!args[i + 1] || args[i + 1].startsWith('--')))
|
|
97
|
+
return fail(`Missing parameter value for ${flag}.`);
|
|
89
98
|
else if (value === undefined)
|
|
90
99
|
value = args[++i];
|
|
91
100
|
if (value === undefined)
|
|
92
101
|
return fail(`Missing parameter value for ${flag}.`);
|
|
93
|
-
|
|
102
|
+
let parsed = value;
|
|
103
|
+
if (type === 'integer') {
|
|
94
104
|
if (!/^-?\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
95
105
|
return fail(`Parameter ${flag} must be a safe integer.`);
|
|
96
|
-
|
|
106
|
+
parsed = Number(value);
|
|
97
107
|
}
|
|
98
|
-
else if (
|
|
108
|
+
else if (type === 'number') {
|
|
99
109
|
if (!value.trim() || !Number.isFinite(Number(value)))
|
|
100
110
|
return fail(`Parameter ${flag} must be a number.`);
|
|
101
|
-
|
|
111
|
+
parsed = Number(value);
|
|
102
112
|
}
|
|
103
|
-
else if (
|
|
113
|
+
else if (type === 'boolean') {
|
|
104
114
|
if (!['true', 'false'].includes(value))
|
|
105
115
|
return fail(`Parameter ${flag} must be true or false.`);
|
|
106
|
-
|
|
116
|
+
parsed = value === 'true';
|
|
117
|
+
}
|
|
118
|
+
if (repeatable) {
|
|
119
|
+
const values = (params[key] ??= []);
|
|
120
|
+
values.push(parsed);
|
|
107
121
|
}
|
|
108
122
|
else
|
|
109
|
-
params[key] =
|
|
123
|
+
params[key] = parsed;
|
|
110
124
|
}
|
|
111
125
|
}
|
|
112
126
|
if (!params || Array.isArray(params) || typeof params !== 'object')
|
package/dist/ctl/service.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CtlError } from './errors.js';
|
|
1
2
|
import { type DsmSession, type JsonRequest } from './transport.js';
|
|
2
3
|
import { CtlStore, type Host } from './store.js';
|
|
3
4
|
import { type Descriptor } from './descriptor.js';
|
|
@@ -11,6 +12,18 @@ export interface DiscoverOptions {
|
|
|
11
12
|
version: string;
|
|
12
13
|
}[]) => Promise<boolean>;
|
|
13
14
|
}
|
|
15
|
+
interface InstalledApp {
|
|
16
|
+
id: string;
|
|
17
|
+
version: string;
|
|
18
|
+
aliases: string[];
|
|
19
|
+
error?: CtlError;
|
|
20
|
+
}
|
|
21
|
+
interface Discovery {
|
|
22
|
+
apps: Descriptor[];
|
|
23
|
+
installed: InstalledApp[];
|
|
24
|
+
diagnostics: any[];
|
|
25
|
+
cacheHits: number;
|
|
26
|
+
}
|
|
14
27
|
export declare class CtlClient {
|
|
15
28
|
store: CtlStore;
|
|
16
29
|
host: Host;
|
|
@@ -24,11 +37,9 @@ export declare class CtlClient {
|
|
|
24
37
|
version: string;
|
|
25
38
|
state?: unknown;
|
|
26
39
|
}[]>;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
cacheHits: number;
|
|
31
|
-
}>;
|
|
40
|
+
private needsApproval;
|
|
41
|
+
selectApp(listing: Discovery, name: string): Descriptor;
|
|
42
|
+
discover(options?: DiscoverOptions): Promise<Discovery>;
|
|
32
43
|
execute(name: string, path: string[], args: string[], options?: DiscoverOptions & {
|
|
33
44
|
yes?: boolean;
|
|
34
45
|
dryRun?: boolean;
|
|
@@ -39,3 +50,4 @@ export declare class CtlClient {
|
|
|
39
50
|
private executeWithin;
|
|
40
51
|
private appCookieSession;
|
|
41
52
|
}
|
|
53
|
+
export {};
|
package/dist/ctl/service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CtlError,
|
|
1
|
+
import { CtlError, ctlError, errorData } from './errors.js';
|
|
2
2
|
import { APP_COOKIE, CTL_TIMEOUT_MS, createRequest } from './transport.js';
|
|
3
3
|
import { DESCRIPTION_METHOD, validateDescriptor, ajv } from './descriptor.js';
|
|
4
4
|
import { runFlow } from './flows.js';
|
|
@@ -41,24 +41,26 @@ export class CtlClient {
|
|
|
41
41
|
return entries.map((p) => { if (typeof p.id !== 'string' || !/^[A-Za-z][A-Za-z0-9_.+-]{0,127}$/.test(p.id) || typeof p.version !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._+~-]{0,63}$/.test(p.version))
|
|
42
42
|
throw new CtlError('CTL_INVENTORY_INVALID', 'Invalid package identity.', 'Check the DSM response.', 5); return { id: p.id, version: p.version, state: p.additional?.status }; });
|
|
43
43
|
}
|
|
44
|
+
needsApproval(pkg) {
|
|
45
|
+
return new CtlError('CTL_APP_NEEDS_APPROVAL', `${pkg.id} ${pkg.version} needs approval`, `run: synopkgland ctl list --app ${pkg.id} --approve-installed --host ${this.host.name}`, 4);
|
|
46
|
+
}
|
|
47
|
+
selectApp(listing, name) {
|
|
48
|
+
const pkg = selectApp(listing.installed, name);
|
|
49
|
+
if (pkg.error)
|
|
50
|
+
throw pkg.error;
|
|
51
|
+
return listing.apps.find(app => app.payload.package.id === pkg.id);
|
|
52
|
+
}
|
|
44
53
|
async discover(options = {}) {
|
|
45
54
|
const session = await this.store.session(this.host);
|
|
46
|
-
let packages = await this.inventory(session)
|
|
55
|
+
let packages = (await this.inventory(session)).map(pkg => {
|
|
56
|
+
const approved = this.host.approved[pkg.id];
|
|
57
|
+
// Previous-version metadata identifies aliases only; it never authorizes a call.
|
|
58
|
+
const description = this.store.cache(this.host, pkg.id, pkg.version)?.description ?? (approved ? this.store.cache(this.host, pkg.id, approved.version)?.description : undefined);
|
|
59
|
+
return { ...pkg, aliases: description?.payload.aliases ?? [] };
|
|
60
|
+
});
|
|
47
61
|
// An exact package filter narrows credential delivery. Alias selection needs the cached descriptors first.
|
|
48
|
-
if (options.app)
|
|
49
|
-
|
|
50
|
-
if (exact)
|
|
51
|
-
packages = [exact];
|
|
52
|
-
else {
|
|
53
|
-
const match = packages.find(p => this.store.cache(this.host, p.id, p.version)?.description?.payload.aliases?.some((a) => a.toLowerCase() === options.app.toLowerCase()));
|
|
54
|
-
if (match)
|
|
55
|
-
packages = [match];
|
|
56
|
-
else {
|
|
57
|
-
const known = packages.flatMap(p => { const d = this.store.cache(this.host, p.id, p.version)?.description; return d ? [p.id, ...d.payload.aliases] : []; });
|
|
58
|
-
choose(options.app, known, 'app');
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
+
if (options.app)
|
|
63
|
+
packages = [selectApp(packages, options.app)];
|
|
62
64
|
const pending = packages.filter(p => this.host.approved[p.id]?.version !== p.version);
|
|
63
65
|
if (pending.length && (options.approve || await options.confirm?.(pending))) {
|
|
64
66
|
for (const p of pending)
|
|
@@ -68,11 +70,14 @@ export class CtlClient {
|
|
|
68
70
|
let cacheHits = 0;
|
|
69
71
|
const results = await boundedMap(packages, 4, async (pkg) => {
|
|
70
72
|
const approval = this.host.approved[pkg.id];
|
|
71
|
-
if (approval?.version !== pkg.version)
|
|
72
|
-
|
|
73
|
+
if (approval?.version !== pkg.version) {
|
|
74
|
+
pkg.error = this.needsApproval(pkg);
|
|
75
|
+
return { error: { package: pkg.id, ...errorData(pkg.error) } };
|
|
76
|
+
}
|
|
73
77
|
const cached = options.refresh ? null : this.store.cache(this.host, pkg.id, pkg.version);
|
|
74
78
|
if (cached?.failure) {
|
|
75
79
|
cacheHits++;
|
|
80
|
+
pkg.error = new CtlError(cached.failure.code, cached.failure.message, cached.failure.hint, 5);
|
|
76
81
|
return { error: { package: pkg.id, ...cached.failure, cached: true } };
|
|
77
82
|
}
|
|
78
83
|
try {
|
|
@@ -87,13 +92,19 @@ export class CtlClient {
|
|
|
87
92
|
throw new CtlError('CTL_DESCRIPTION_UNAVAILABLE', `Description refused (${String(response?.error?.code ?? 'invalid response')}).`, 'Use --refresh after updating the App.', 5);
|
|
88
93
|
description = validateDescriptor(response.data, pkg.id, pkg.version);
|
|
89
94
|
}
|
|
90
|
-
if (approval.hash && approval.hash !== description.sha256 && !options.approve)
|
|
91
|
-
|
|
95
|
+
if (approval.hash && approval.hash !== description.sha256 && !options.approve) {
|
|
96
|
+
// Remember the observed change so a later cache hit cannot revive the old approval.
|
|
97
|
+
this.store.putCache(this.host, pkg.id, pkg.version, description);
|
|
98
|
+
pkg.aliases = [...new Set([...pkg.aliases, ...description.payload.aliases])];
|
|
99
|
+
throw this.needsApproval(pkg);
|
|
100
|
+
}
|
|
101
|
+
pkg.aliases = description.payload.aliases;
|
|
92
102
|
approval.hash = description.sha256;
|
|
93
103
|
this.store.putCache(this.host, pkg.id, pkg.version, description);
|
|
94
104
|
return { description };
|
|
95
105
|
}
|
|
96
106
|
catch (error) {
|
|
107
|
+
pkg.error = ctlError(error);
|
|
97
108
|
const failure = errorData(error);
|
|
98
109
|
if (failure.code === 'CTL_DESCRIPTION_UNAVAILABLE')
|
|
99
110
|
this.store.putFailure(this.host, pkg.id, pkg.version, failure);
|
|
@@ -102,7 +113,7 @@ export class CtlClient {
|
|
|
102
113
|
});
|
|
103
114
|
this.host.lastUsed = new Date().toISOString();
|
|
104
115
|
this.store.save(this.host);
|
|
105
|
-
return { apps: results.flatMap(r => r.description ? [r.description] : []), diagnostics: results.flatMap(r => r.error ? [r.error] : []), cacheHits };
|
|
116
|
+
return { apps: results.flatMap(r => r.description ? [r.description] : []), installed: packages, diagnostics: results.flatMap(r => r.error ? [r.error] : []), cacheHits };
|
|
106
117
|
}
|
|
107
118
|
async execute(name, path, args, options = {}) {
|
|
108
119
|
try {
|
|
@@ -116,7 +127,7 @@ export class CtlClient {
|
|
|
116
127
|
const started = performance.now();
|
|
117
128
|
this.until = started + (options.timeoutMs ?? CTL_TIMEOUT_MS);
|
|
118
129
|
const listing = await this.discover(options);
|
|
119
|
-
const app = selectApp(listing
|
|
130
|
+
const app = this.selectApp(listing, name);
|
|
120
131
|
const { id, operation } = selectOperation(app, path);
|
|
121
132
|
const params = operationInput(operation, args, options.stdin, `run: synopkgland ctl ${name} ${operation.cli.join(' ')} --host ${this.host.name} --help`);
|
|
122
133
|
const output = new SensitiveOutput(params, operation.paramsSchema);
|
package/dist/error-registry.js
CHANGED
|
@@ -33,7 +33,7 @@ export const ERROR_REGISTRY = {
|
|
|
33
33
|
INIT_HEALTH_INVALID: { code: "INIT_HEALTH_INVALID", description: "The Compose readiness selection is invalid.", suggestion: "Supply both --health-path and --health-status with the actual endpoint and expected 2xx response; redirects are not followed." },
|
|
34
34
|
INIT_UPSTREAM_IMAGE_REQUIRED: { code: "INIT_UPSTREAM_IMAGE_REQUIRED", description: "The official upstream image repository was not selected.", suggestion: "Use init --upstream-fork --upstream-image <official-repository> for Compose." },
|
|
35
35
|
INIT_UPSTREAM_IMAGE_INVALID: { code: "INIT_UPSTREAM_IMAGE_INVALID", description: "The upstream image repository is invalid.", suggestion: "Supply the official repository without a tag or digest; upgrade.sh selects the matching release and resolves its digest." },
|
|
36
|
-
|
|
36
|
+
CTL_APP_NEEDS_APPROVAL: { "code": "CTL_APP_NEEDS_APPROVAL", "description": "The installed App needs approval or re-approval.", "suggestion": "Run synopkgland ctl list --app <package-id> --approve-installed --host <host>." },
|
|
37
37
|
COMPOSE_HEALTH_REQUIRED: { code: "COMPOSE_HEALTH_REQUIRED", description: "Compose web readiness was not selected.", suggestion: "Set compose.web.health.path and expected 2xx status for the actual App; redirects are not followed." },
|
|
38
38
|
CTL_UNSUPPORTED_BACKEND: { "code": "CTL_UNSUPPORTED_BACKEND", "description": "Automatic descriptions require a native SDK backend.", "suggestion": "Disable ctl for binary/Compose wrappers or implement the public contract separately." },
|
|
39
39
|
CTL_DESCRIBE_REQUIRED: { "code": "CTL_DESCRIBE_REQUIRED", "description": "The Go backend has no build-only registry exporter.", "suggestion": "Update the native template and implement --describe using api.DescribeRoutes." },
|
|
@@ -12,6 +12,20 @@ export interface InstallOptions {
|
|
|
12
12
|
dryRun?: boolean;
|
|
13
13
|
}
|
|
14
14
|
export declare function installPackage(spec: string, options?: InstallOptions, connection?: NasConnection): Promise<{
|
|
15
|
+
message?: string | undefined;
|
|
16
|
+
host: string;
|
|
17
|
+
target: NasTarget;
|
|
18
|
+
source: string;
|
|
19
|
+
packages: {
|
|
20
|
+
signed?: boolean | undefined;
|
|
21
|
+
package: string;
|
|
22
|
+
version: string;
|
|
23
|
+
size: number;
|
|
24
|
+
action: string;
|
|
25
|
+
}[];
|
|
26
|
+
parameters: string[];
|
|
27
|
+
} | {
|
|
28
|
+
message?: string | undefined;
|
|
15
29
|
host: string;
|
|
16
30
|
target: NasTarget;
|
|
17
31
|
source: string;
|
|
@@ -26,6 +40,7 @@ export declare function installPackage(spec: string, options?: InstallOptions, c
|
|
|
26
40
|
dryRun: boolean;
|
|
27
41
|
} | {
|
|
28
42
|
status: import("./catalog-install.js").InstalledPackageState | undefined;
|
|
43
|
+
message?: string | undefined;
|
|
29
44
|
host: string;
|
|
30
45
|
target: NasTarget;
|
|
31
46
|
source: string;
|
|
@@ -122,6 +122,8 @@ export async function installPackage(spec, options = {}, connection) {
|
|
|
122
122
|
if (installed.has(item.package) &&
|
|
123
123
|
compareVersions(item.version, installed.get(item.package)) < 0)
|
|
124
124
|
throw fail("PACKAGE_DOWNGRADE_REFUSED", `Refusing downgrade of ${item.package}`, "Choose the installed version or a newer release");
|
|
125
|
+
const changes = plan.filter((item) => !installed.has(item.package) ||
|
|
126
|
+
compareVersions(item.version, installed.get(item.package)) !== 0);
|
|
125
127
|
const publicPlan = {
|
|
126
128
|
host: nas.name,
|
|
127
129
|
target,
|
|
@@ -130,21 +132,30 @@ export async function installPackage(spec, options = {}, connection) {
|
|
|
130
132
|
package: item.package,
|
|
131
133
|
version: item.version,
|
|
132
134
|
size: item.size,
|
|
133
|
-
action: installed.has(item.package)
|
|
135
|
+
action: !installed.has(item.package)
|
|
136
|
+
? "install"
|
|
137
|
+
: compareVersions(item.version, installed.get(item.package)) === 0
|
|
138
|
+
? "none"
|
|
139
|
+
: "upgrade",
|
|
134
140
|
...(item.package === root.package && report
|
|
135
141
|
? { signed: report.signed }
|
|
136
142
|
: {}),
|
|
137
143
|
})),
|
|
138
144
|
parameters: Object.keys(options.param ?? {}),
|
|
145
|
+
...(!changes.length
|
|
146
|
+
? { message: `${root.package}@${root.version} is already up to date; no package changes required.` }
|
|
147
|
+
: {}),
|
|
139
148
|
};
|
|
140
149
|
if (options.dryRun)
|
|
141
150
|
return { dryRun: true, ...publicPlan };
|
|
151
|
+
if (!changes.length)
|
|
152
|
+
return publicPlan;
|
|
142
153
|
await requireAdministrator(nas);
|
|
143
|
-
await confirm(`Install ${
|
|
154
|
+
await confirm(`Install ${changes.map((e) => e.package + "@" + e.version).join(", ")}`, options.yes);
|
|
144
155
|
// Download, verify and inspect the entire plan before the first NAS write.
|
|
145
156
|
const files = new Map();
|
|
146
157
|
const images = new Map();
|
|
147
|
-
for (const entry of
|
|
158
|
+
for (const entry of changes) {
|
|
148
159
|
let file = localFile;
|
|
149
160
|
if (!local || entry.package !== root.package) {
|
|
150
161
|
const response = await fetchPackageSource(entry.link);
|
|
@@ -163,7 +174,7 @@ export async function installPackage(spec, options = {}, connection) {
|
|
|
163
174
|
files.set(entry.package, file);
|
|
164
175
|
images.set(entry.package, containerImageReferences(checked));
|
|
165
176
|
}
|
|
166
|
-
for (const entry of
|
|
177
|
+
for (const entry of changes)
|
|
167
178
|
await installPackageFromLocalSpk({
|
|
168
179
|
dsmOrigin: nas.session.origin,
|
|
169
180
|
cookieHeader: nas.session.cookie,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { goPrivateCovers } from './go-env.js';
|
|
2
2
|
import { type ExecutorManifest } from './sdk-executor-manifest.js';
|
|
3
3
|
/** Pinned SDK for CLI-owned controllers and Compose planning; App pins take precedence. */
|
|
4
|
-
export declare const DEFAULT_SDK_VERSION = "0.41.
|
|
4
|
+
export declare const DEFAULT_SDK_VERSION = "0.41.1";
|
|
5
5
|
export declare const SDK_PACKAGE = "@synopackageland/sdk";
|
|
6
6
|
export declare const SDK_MODULE = "github.com/synopackageland/syno-pkg-land-sdk";
|
|
7
7
|
export declare const EXECUTOR_MODULE = "github.com/synopackageland/syno-pkg-land-sdk/executor";
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { readExecutorManifest, predatesExecutorManifest } from './sdk-executor-manifest.js';
|
|
7
7
|
/** Pinned SDK for CLI-owned controllers and Compose planning; App pins take precedence. */
|
|
8
|
-
export const DEFAULT_SDK_VERSION = '0.41.
|
|
8
|
+
export const DEFAULT_SDK_VERSION = '0.41.1';
|
|
9
9
|
export const SDK_PACKAGE = '@synopackageland/sdk';
|
|
10
10
|
export const SDK_MODULE = 'github.com/synopackageland/syno-pkg-land-sdk';
|
|
11
11
|
export const EXECUTOR_MODULE = `${SDK_MODULE}/executor`;
|
package/docs/ctl-external.md
CHANGED
|
@@ -68,6 +68,22 @@ selected live descriptor against its approved hash. Zero or multiple matching
|
|
|
68
68
|
Apps refuse; revoke the unwanted approval or use distinct protocol IDs. A cache
|
|
69
69
|
is only a recipient selector and never grants tool authority.
|
|
70
70
|
|
|
71
|
+
For ordinary `ctl <alias|package> …` and help requests, an installed App awaiting
|
|
72
|
+
first approval, a new version's approval, or approval of a changed description
|
|
73
|
+
returns `CTL_APP_NEEDS_APPROVAL` (exit 4). For example, the message is
|
|
74
|
+
`SynoReclaim 1.2.0 needs approval` with an App-scoped command:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
synopkgland ctl list --app SynoReclaim --approve-installed --host controller
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Obtain the user's authorization before running the hint. Cached aliases from the
|
|
81
|
+
previously approved version still identify an upgraded installed App, but cannot
|
|
82
|
+
authorize execution. Before an alias has been discovered, use the package ID for
|
|
83
|
+
first approval. Unknown-name suggestions appear only if no installed package ID
|
|
84
|
+
or known alias matches. `ctl list` includes pending approvals in its diagnostics;
|
|
85
|
+
external calls retain the separate protocol/pin checks described below.
|
|
86
|
+
|
|
71
87
|
Only a named, registered host and its OS-stored session are used. Ambient
|
|
72
88
|
`DSM_*` credentials/URL cannot create or replace this login. HTTPS requires the
|
|
73
89
|
stored SHA-256 certificate pin and refuses rotation before sending HTTP headers,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.1",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@napi-rs/keyring": "^2.1.1",
|
|
31
|
-
"@synopackageland/templates": "~0.
|
|
31
|
+
"@synopackageland/templates": "~0.37.0",
|
|
32
32
|
"ajv": "^8.17.1",
|
|
33
33
|
"commander": "^13.1.0",
|
|
34
34
|
"json-canonicalize": "^3.0.1",
|
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
|
-
The CLI installs templates through `~0.
|
|
8
|
+
The CLI installs templates through `~0.37.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
|
package/skills/reference/ctl.md
CHANGED
|
@@ -10,8 +10,30 @@ Use this for operating installed Apps, rather than building/deploying projects.
|
|
|
10
10
|
4. Use the discovered alias, CLI path and typed flags with an explicit `--host nas`; inspect `--help` and `--dry-run` before execution. For example, `synopkgland ctl notes list --status pending --host nas --dry-run`. Use the real command/schema from discovery, not this illustrative name.
|
|
11
11
|
5. Reads need no write confirmation. For authorized write/destructive commands, add `--yes`; without it the CLI returns a plan and confirmation error. A --yes flag does not replace the user's authorization or the App's backend checks.
|
|
12
12
|
|
|
13
|
+
Typed flags accept strings, integers, numbers and booleans, including arrays of
|
|
14
|
+
these scalar types. Repeat an array flag for each item, for example
|
|
15
|
+
`--targets /volume1 --targets /volume2`; one occurrence still produces an array.
|
|
16
|
+
Use `--flag=value` when a string item starts with `--`. Commas are literal string
|
|
17
|
+
content, not array separators. Items and the complete array are checked against
|
|
18
|
+
the command schema. Command `--help` lists each supported flag's `valueType` and
|
|
19
|
+
`repeatable` status and renders scalar-array examples as repeated flags.
|
|
20
|
+
Objects, arrays of objects, nested arrays and arrays without a scalar item type
|
|
21
|
+
require `--input <file|->`; use JSON input for an explicit empty array too.
|
|
22
|
+
Plans keep schema-marked private paths and other sensitive values redacted.
|
|
23
|
+
|
|
13
24
|
With multiple hosts, select a default with `synopkgland host default <name>` or specify --host. `--host a,b` and --all-hosts return per-host results, with exit 8 on partial failure. Do not retry a write merely because one host failed; query its state first. --refresh rereads descriptions; unchanged package versions use the host/account/package/version/hash cache. Never assume cached metadata grants resource permission.
|
|
14
25
|
|
|
26
|
+
An installed App awaiting first approval, approval of a new version, or approval
|
|
27
|
+
of a changed description returns `CTL_APP_NEEDS_APPROVAL` (exit 4) when selected
|
|
28
|
+
by package ID or a known cached alias, including help requests. The message names
|
|
29
|
+
the installed package and version, for example `SynoReclaim 1.2.0 needs approval`.
|
|
30
|
+
Obtain the user's authorization, then follow the exact App-scoped hint:
|
|
31
|
+
`synopkgland ctl list --app SynoReclaim --approve-installed --host <profile>`.
|
|
32
|
+
Previous-version aliases identify the installed App only; they never authorize
|
|
33
|
+
execution. An alias not yet discovered requires the package ID for first approval.
|
|
34
|
+
`CTL_APP_UNKNOWN` and “Did you mean …” apply only when no installed package ID or
|
|
35
|
+
known alias matches. `ctl list` reports pending approvals in `data.diagnostics`.
|
|
36
|
+
|
|
15
37
|
Use `host show nas` to inspect approvals/pins, `host revoke nas PackageID` to remove an App approval, `host logout nas` to retain registration, and `host remove nas --yes` to delete sessions, device token, approvals and cache. A failed NAS logout is reported independently from successful local removal.
|
|
16
38
|
|
|
17
39
|
Stdout defaults to JSON without a TTY. Success is {ok:true,data}; failure is {ok:false,error:{code,message,hint,allowed?}}. Multi-host output uses {ok,results:[{host,ok,data|error}]}. Follow the concrete hint and complete allowed list; do not guess another method or bypass ctl with raw HTTP. Exit codes: 2 authentication, 3 parameters/names, 4 permissions/trust, 5 App/contract, 6 network, 7 confirmation, 8 partial failure.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Native installation settings
|
|
2
2
|
|
|
3
|
-
Use SDK/App 0.
|
|
3
|
+
Use SDK/App 0.41.1, CLI 0.42.0 and templates 0.37.0. Configure installation through the manifest and generated wizard; do not add App ACL writers, root hooks or independent port state.
|
|
4
4
|
|
|
5
5
|
```yaml
|
|
6
6
|
install:
|