@synopackageland/cli 0.14.1 → 0.15.0
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 +10 -0
- package/README.md +26 -16
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -846
- package/dist/build-spk/resource-worker.d.ts +2 -14
- package/dist/build-spk/resource-worker.js +3 -166
- package/dist/cli.js +208 -56
- package/dist/compose/analyze.d.ts +3 -61
- package/dist/compose/analyze.js +35 -776
- package/dist/compose/env.d.ts +5 -3
- package/dist/compose/env.js +17 -25
- package/dist/compose/runner.js +23 -6
- package/dist/contract/web-launch.d.ts +1 -12
- package/dist/contract/web-launch.js +10 -74
- package/dist/ctl/cli.d.ts +36 -0
- package/dist/ctl/cli.js +218 -0
- package/dist/ctl/descriptor.d.ts +81 -0
- package/dist/ctl/descriptor.js +93 -0
- package/dist/ctl/dsm-auth.d.ts +10 -0
- package/dist/ctl/dsm-auth.js +30 -0
- package/dist/ctl/errors.d.ts +17 -0
- package/dist/ctl/errors.js +32 -0
- package/dist/ctl/flows.d.ts +11 -0
- package/dist/ctl/flows.js +52 -0
- package/dist/ctl/input.d.ts +20 -0
- package/dist/ctl/input.js +128 -0
- package/dist/ctl/json.d.ts +2 -0
- package/dist/ctl/json.js +38 -0
- package/dist/ctl/redaction.d.ts +15 -0
- package/dist/ctl/redaction.js +198 -0
- package/dist/ctl/service.d.ts +38 -0
- package/dist/ctl/service.js +156 -0
- package/dist/ctl/store.d.ts +51 -0
- package/dist/ctl/store.js +93 -0
- package/dist/ctl/transfers.d.ts +14 -0
- package/dist/ctl/transfers.js +74 -0
- package/dist/ctl/transport.d.ts +11 -0
- package/dist/ctl/transport.js +79 -0
- package/dist/deploy/deploy.d.ts +4 -3
- package/dist/deploy/deploy.js +5 -210
- package/dist/deploy/plan.js +26 -7
- package/dist/dev.js +5 -4
- package/dist/error-registry.js +1020 -834
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +1 -12
- package/dist/skill.js +51 -9
- package/dist/standalone/api-description-inspect.d.ts +4 -0
- package/dist/standalone/api-description-inspect.js +12 -0
- package/dist/standalone/api-description.d.ts +7 -0
- package/dist/standalone/api-description.js +73 -0
- package/dist/standalone/build.d.ts +3 -0
- package/dist/standalone/build.js +140 -33
- package/dist/standalone/compose-errors.d.ts +3 -0
- package/dist/standalone/compose-errors.js +243 -0
- package/dist/standalone/compose.d.ts +97 -10
- package/dist/standalone/compose.js +306 -75
- package/dist/standalone/daemon.js +111 -29
- package/dist/standalone/dsm-window.js +39 -8
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +12 -0
- package/dist/standalone/inspect-spk.js +666 -136
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +8 -1
- package/dist/standalone/manifest.js +303 -86
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +138 -39
- package/dist/template-paths.d.ts +0 -2
- package/dist/template-paths.js +0 -5
- package/dist/test-command.d.ts +1 -1
- package/dist/test-command.js +1 -1
- package/dist/validate.d.ts +3 -3
- package/dist/validate.js +32 -140
- package/package.json +5 -3
- package/schema/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +3 -1
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +8 -17
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +4 -0
- package/skills/reference/identity-gateway.md +8 -144
- package/skills/reference/wrap.md +10 -39
- package/assets/go/daemon/fixture_linux_test.go +0 -15
- package/assets/go/daemon/fixture_other_test.go +0 -7
- package/assets/go/daemon/go.mod +0 -5
- package/assets/go/daemon/main.go +0 -636
- package/assets/go/daemon/main_test.go +0 -315
- package/assets/go/daemon/network.go +0 -107
- package/assets/go/daemon/network_test.go +0 -86
- package/assets/go/daemon/process_linux.go +0 -55
- package/assets/go/daemon/process_linux_test.go +0 -161
- package/assets/go/daemon/process_other.go +0 -14
- package/assets/go/daemon/testdata/child.go +0 -44
- package/assets/go/gateway/auth.go +0 -101
- package/assets/go/gateway/auth_test.go +0 -216
- package/assets/go/gateway/cgi.go +0 -133
- package/assets/go/gateway/cgi_test.go +0 -156
- package/assets/go/gateway/csrf.go +0 -73
- package/assets/go/gateway/csrf_test.go +0 -155
- package/assets/go/gateway/directory.go +0 -108
- package/assets/go/gateway/directory_test.go +0 -202
- package/assets/go/gateway/gateway.go +0 -412
- package/assets/go/gateway/gateway_test.go +0 -676
- package/assets/go/gateway/go.mod +0 -5
- package/assets/go/gateway/groups.go +0 -66
- package/assets/go/gateway/main.go +0 -45
- package/dist/build-spk/gateway.d.ts +0 -31
- package/dist/build-spk/gateway.js +0 -60
- package/dist/compose/privilege.d.ts +0 -14
- package/dist/compose/privilege.js +0 -102
package/dist/init.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { goChildEnv } from "./runtime/go-env.js";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { parse, parseDocument } from "yaml";
|
|
4
4
|
import { setTopLevel } from "./yaml-edit.js";
|
|
5
|
-
import { capabilityNames, capabilitySettings } from "./capabilities.js";
|
|
6
|
-
import { MOVE_FROM_STAGING } from
|
|
7
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { capabilityNames, capabilitySettings, } from "./capabilities.js";
|
|
6
|
+
import { MOVE_FROM_STAGING } from "./runtime/delegate-contract.js";
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
8
8
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
9
9
|
import { APP_SCRIPTS_DIR } from "./standalone/app-scripts.js";
|
|
10
10
|
import { SDK_MODULE, SDK_PACKAGE, sdkSource } from "./runtime/sdk-source.js";
|
|
11
11
|
import { templatesRoot } from "./template-paths.js";
|
|
12
|
-
import { BASE_BORROWED, BRIDGE_TEMPLATES, borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS } from "./capability-overlays.js";
|
|
13
|
-
import { copyNodeSdk, copyPythonSdk, NODE_SDK_PACKAGE } from "./standalone/bridge-app.js";
|
|
12
|
+
import { BASE_BORROWED, BRIDGE_TEMPLATES, borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS, } from "./capability-overlays.js";
|
|
13
|
+
import { copyNodeSdk, copyPythonSdk, NODE_SDK_PACKAGE, } from "./standalone/bridge-app.js";
|
|
14
14
|
import { API_PERMISSIONS_FILE } from "./standalone/api-permissions.js";
|
|
15
|
-
import { resolveMinDsm, assertContainerMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast } from "./build-spk/dsm-version.js";
|
|
15
|
+
import { resolveMinDsm, assertContainerMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, } from "./build-spk/dsm-version.js";
|
|
16
|
+
import { composeInputs } from "./standalone/compose.js";
|
|
16
17
|
import { readSharedFolder } from "./standalone/shared-folder.js";
|
|
17
18
|
const TEMPLATE_NAMES = [
|
|
18
19
|
"compose",
|
|
@@ -26,13 +27,13 @@ const isNativeTemplate = (template) => NATIVE_TEMPLATES.includes(template);
|
|
|
26
27
|
const isBridgeTemplate = (template) => BRIDGE_TEMPLATES.includes(template);
|
|
27
28
|
/** Where each native template keeps its API permissions. */
|
|
28
29
|
const PERMISSIONS_FILE = {
|
|
29
|
-
"native-go": `backend/${API_PERMISSIONS_FILE.go}`,
|
|
30
|
-
"native-
|
|
30
|
+
"native-go": `backend/${API_PERMISSIONS_FILE.go}`,
|
|
31
|
+
"native-python": `backend/app/${API_PERMISSIONS_FILE.python}`,
|
|
32
|
+
"native-node": `backend/app/${API_PERMISSIONS_FILE.node}`,
|
|
33
|
+
"native-rust": `backend/${API_PERMISSIONS_FILE.rust}`,
|
|
31
34
|
};
|
|
32
35
|
/** Templates whose App runs through the DSM docker-project resource worker. */
|
|
33
|
-
const CONTAINER_TEMPLATES = [
|
|
34
|
-
"compose",
|
|
35
|
-
];
|
|
36
|
+
const CONTAINER_TEMPLATES = ["compose"];
|
|
36
37
|
/** The container templates need DSM 7.2.1 or later; native-go and binary need 7.1. */
|
|
37
38
|
export function isContainerTemplate(template) {
|
|
38
39
|
return CONTAINER_TEMPLATES.includes(template);
|
|
@@ -46,26 +47,35 @@ export function listTemplates() {
|
|
|
46
47
|
/** New native scaffolds opt in explicitly; existing manifests are never migrated. */
|
|
47
48
|
export function nativeCapabilitySettings(names) {
|
|
48
49
|
const settings = capabilitySettings(names);
|
|
49
|
-
if (names.includes(
|
|
50
|
+
if (names.includes("delegate"))
|
|
50
51
|
settings.delegate = {
|
|
51
|
-
scope:
|
|
52
|
+
scope: "install-selected",
|
|
53
|
+
operations: [MOVE_FROM_STAGING],
|
|
54
|
+
destinations: ["save"],
|
|
55
|
+
lifetime: "until-revoked",
|
|
52
56
|
};
|
|
53
57
|
return settings;
|
|
54
58
|
}
|
|
55
59
|
/** Templates whose identity is filled in at init: every generated file may carry a placeholder. */
|
|
56
|
-
const IDENTIFIED = [...NATIVE_TEMPLATES, "binary"];
|
|
60
|
+
const IDENTIFIED = [...NATIVE_TEMPLATES, "binary", "compose"];
|
|
57
61
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
58
62
|
/** npm never publishes files named .gitignore, so templates ship them as `gitignore`. */
|
|
59
63
|
const RENAMED = { gitignore: ".gitignore" };
|
|
60
64
|
function words(dir) {
|
|
61
|
-
return basename(dir)
|
|
65
|
+
return basename(dir)
|
|
66
|
+
.split(/[^A-Za-z0-9]+/)
|
|
67
|
+
.filter(Boolean);
|
|
62
68
|
}
|
|
63
69
|
function defaultPackageId(dir) {
|
|
64
|
-
const id = words(dir)
|
|
70
|
+
const id = words(dir)
|
|
71
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
72
|
+
.join("");
|
|
65
73
|
return PACKAGE_ID.test(id) ? id : "MyPackage";
|
|
66
74
|
}
|
|
67
75
|
function defaultDisplayName(dir) {
|
|
68
|
-
const name = words(dir)
|
|
76
|
+
const name = words(dir)
|
|
77
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
78
|
+
.join(" ");
|
|
69
79
|
return name || "My Package";
|
|
70
80
|
}
|
|
71
81
|
function templateFiles(root, dir = root) {
|
|
@@ -84,7 +94,7 @@ function targetName(file) {
|
|
|
84
94
|
}
|
|
85
95
|
/** Native templates default to the Vue shell; the CLI and the library entry point agree. */
|
|
86
96
|
function resolvedUi(template, options) {
|
|
87
|
-
return isNativeTemplate(template) ? options.ui ?? "vue" : options.ui;
|
|
97
|
+
return isNativeTemplate(template) ? (options.ui ?? "vue") : options.ui;
|
|
88
98
|
}
|
|
89
99
|
/**
|
|
90
100
|
* Copy one template tree into a project. A template directory may carry a local
|
|
@@ -92,18 +102,32 @@ function resolvedUi(template, options) {
|
|
|
92
102
|
* to a generated project (and would otherwise shadow the project's own install).
|
|
93
103
|
*/
|
|
94
104
|
export function copyTemplateTree(source, target) {
|
|
95
|
-
cpSync(source, target, {
|
|
105
|
+
cpSync(source, target, {
|
|
106
|
+
recursive: true,
|
|
107
|
+
filter: (path) => !isTemplateBuildArtifact(path),
|
|
108
|
+
});
|
|
96
109
|
}
|
|
97
110
|
function isTemplateBuildArtifact(path) {
|
|
98
111
|
const name = basename(path);
|
|
99
|
-
return name === "node_modules" || name === "dist" || name === ".go-results.json";
|
|
112
|
+
return (name === "node_modules" || name === "dist" || name === ".go-results.json");
|
|
100
113
|
}
|
|
101
114
|
export function initFromTemplate(template, targetDir, options = {}) {
|
|
102
115
|
if (!isTemplateName(template))
|
|
103
116
|
throw new Error(`[INIT_TEMPLATE_UNKNOWN] Unknown template: ${template}`);
|
|
104
117
|
const requested = capabilityNames(options.with ?? []);
|
|
105
|
-
const capability = isNativeTemplate(template)
|
|
106
|
-
|
|
118
|
+
const capability = isNativeTemplate(template)
|
|
119
|
+
? capabilityOverlays(template, requested)
|
|
120
|
+
: {
|
|
121
|
+
overlays: [],
|
|
122
|
+
borrowed: [],
|
|
123
|
+
removed: [],
|
|
124
|
+
permissions: {},
|
|
125
|
+
implied: [],
|
|
126
|
+
methods: [],
|
|
127
|
+
};
|
|
128
|
+
const caps = capabilityNames([
|
|
129
|
+
...new Set([...(capability.implied ?? []), ...requested]),
|
|
130
|
+
]);
|
|
107
131
|
if (caps.includes("login") && resolvedUi(template, options) === "plain")
|
|
108
132
|
throw new Error("[CAPABILITY_COMBINATION_INVALID] login needs a standalone sign-in page; plain UI does not support it, use --ui vue.");
|
|
109
133
|
if (caps.includes("delegate") && resolvedUi(template, options) === "plain")
|
|
@@ -112,7 +136,9 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
112
136
|
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --from applies only to compose.");
|
|
113
137
|
if (caps.length && !isNativeTemplate(template))
|
|
114
138
|
throw new Error("[CAPABILITY_UNSUPPORTED] binary/compose does not support --with capabilities.");
|
|
115
|
-
const localSdk = isNativeTemplate(template) && process.env.SYNOPKGLAND_SDK_DIR
|
|
139
|
+
const localSdk = isNativeTemplate(template) && process.env.SYNOPKGLAND_SDK_DIR
|
|
140
|
+
? resolve(process.env.SYNOPKGLAND_SDK_DIR)
|
|
141
|
+
: undefined;
|
|
116
142
|
if (localSdk)
|
|
117
143
|
assertLocalSdkReady(localSdk, resolvedUi(template, options) === "vue");
|
|
118
144
|
const source = join(templatesRoot(), template);
|
|
@@ -126,23 +152,47 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
126
152
|
const substitutions = substitutionsFor(template, targetDir, options);
|
|
127
153
|
const ui = resolvedUi(template, options);
|
|
128
154
|
const rootExample = caps.includes("root-ops");
|
|
129
|
-
if (rootExample &&
|
|
155
|
+
if (rootExample &&
|
|
156
|
+
minDsm !== undefined &&
|
|
157
|
+
!isDsmAtLeast(minDsm, DEFAULT_MIN_DSM)) {
|
|
130
158
|
throw new Error(`[MIN_DSM_INVALID] root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
|
|
131
159
|
}
|
|
132
|
-
const vue = ui === "vue" && isNativeTemplate(template)
|
|
160
|
+
const vue = ui === "vue" && isNativeTemplate(template)
|
|
161
|
+
? VUE_OVERLAYS[template]
|
|
162
|
+
: { overlays: [], borrowed: [] };
|
|
133
163
|
const base = isNativeTemplate(template) ? BASE_BORROWED[template] : [];
|
|
134
164
|
// Later borrowed files replace earlier ones, as later overlays do.
|
|
135
|
-
const borrowed = borrowedFiles(templatesRoot(), [
|
|
136
|
-
|
|
165
|
+
const borrowed = borrowedFiles(templatesRoot(), [
|
|
166
|
+
...base,
|
|
167
|
+
...vue.borrowed,
|
|
168
|
+
...capability.borrowed,
|
|
169
|
+
]);
|
|
170
|
+
const overlays = [...vue.overlays, ...capability.overlays].map((name) => join(templatesRoot(), name));
|
|
137
171
|
// Shared legal resources also live in the CLI package; only the template
|
|
138
172
|
// terms are an unconditional part of a generated project.
|
|
139
173
|
const shared = join(templatesRoot(), "_shared");
|
|
140
174
|
// dsm-scripts/: the DSM lifecycle scripts every project starts with (doing nothing); older templates have none.
|
|
141
|
-
const sharedScripts = existsSync(join(shared, APP_SCRIPTS_DIR))
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
175
|
+
const sharedScripts = existsSync(join(shared, APP_SCRIPTS_DIR))
|
|
176
|
+
? readdirSync(join(shared, APP_SCRIPTS_DIR))
|
|
177
|
+
.sort()
|
|
178
|
+
.map((name) => `${APP_SCRIPTS_DIR}/${name}`)
|
|
179
|
+
: [];
|
|
180
|
+
const sharedFiles = [
|
|
181
|
+
"LICENSES/TEMPLATE.txt",
|
|
182
|
+
"AGENTS.md",
|
|
183
|
+
"CLAUDE.md",
|
|
184
|
+
...sharedScripts,
|
|
185
|
+
];
|
|
186
|
+
const files = [
|
|
187
|
+
...templateFiles(source),
|
|
188
|
+
...sharedFiles,
|
|
189
|
+
...overlays.flatMap((root) => templateFiles(root)),
|
|
190
|
+
...borrowed.map(borrowedTarget),
|
|
191
|
+
...(ui === "vue" ? ["frontend/third-party-notices.mjs"] : []),
|
|
192
|
+
];
|
|
193
|
+
const collisions = files
|
|
194
|
+
.map(targetName)
|
|
195
|
+
.filter((file) => existsSync(join(targetDir, file)));
|
|
146
196
|
if (collisions.length > 0) {
|
|
147
197
|
throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} already has ${collisions.join(", ")}; init does not overwrite existing files. Use an empty directory.`);
|
|
148
198
|
}
|
|
@@ -154,16 +204,19 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
154
204
|
}
|
|
155
205
|
for (const overlay of overlays)
|
|
156
206
|
copyTemplateTree(overlay, targetDir);
|
|
157
|
-
for (const dir of
|
|
207
|
+
for (const dir of ui === "vue" ? ["frontend"] : []) {
|
|
158
208
|
cpSync(join(shared, "third-party-notices.mjs"), join(targetDir, dir, "third-party-notices.mjs"));
|
|
159
209
|
}
|
|
160
210
|
for (const path of borrowed) {
|
|
161
|
-
mkdirSync(dirname(join(targetDir, borrowedTarget(path))), {
|
|
211
|
+
mkdirSync(dirname(join(targetDir, borrowedTarget(path))), {
|
|
212
|
+
recursive: true,
|
|
213
|
+
});
|
|
162
214
|
cpSync(join(templatesRoot(), path), join(targetDir, borrowedTarget(path)));
|
|
163
215
|
}
|
|
164
216
|
for (const file of capability.removed)
|
|
165
217
|
rmSync(join(targetDir, file));
|
|
166
|
-
if (Object.keys(capability.permissions).length &&
|
|
218
|
+
if (Object.keys(capability.permissions).length &&
|
|
219
|
+
isNativeTemplate(template)) {
|
|
167
220
|
const path = join(targetDir, PERMISSIONS_FILE[template]);
|
|
168
221
|
const policy = JSON.parse(readFileSync(path, "utf8"));
|
|
169
222
|
Object.assign(policy.methods, capability.permissions);
|
|
@@ -183,15 +236,18 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
183
236
|
if (capability.methods?.length && template === "native-rust") {
|
|
184
237
|
// A cross-compiled Rust App cannot describe itself: its manifest lists the methods.
|
|
185
238
|
const manifest = join(targetDir, "dsm-package.yaml");
|
|
186
|
-
writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^( {2}methods: \[.*)\]$/m, (_, list) => `${list}${capability.methods.map(name => `, ${name}`).join("")}]`));
|
|
239
|
+
writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^( {2}methods: \[.*)\]$/m, (_, list) => `${list}${capability.methods.map((name) => `, ${name}`).join("")}]`));
|
|
187
240
|
}
|
|
188
241
|
if (minDsm) {
|
|
189
|
-
const manifest = join(targetDir,
|
|
242
|
+
const manifest = join(targetDir, "dsm-package.yaml");
|
|
190
243
|
writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^os_min_ver:.*$/m, `os_min_ver: "${minDsm}"`));
|
|
191
244
|
}
|
|
192
245
|
if (ui === "vue") {
|
|
193
246
|
const manifest = join(targetDir, "dsm-package.yaml");
|
|
194
|
-
writeFileSync(manifest, readFileSync(manifest, "utf8")
|
|
247
|
+
writeFileSync(manifest, readFileSync(manifest, "utf8")
|
|
248
|
+
.replace(" dir: ui", " dir: frontend/dist\n build: {cwd: frontend, command: [npm, run, build]}")
|
|
249
|
+
.replace(" launch: new-page\n", "")
|
|
250
|
+
.replace("Non-UI-SDK frontends open a full page", "UI SDK frontends open a DSM window"));
|
|
195
251
|
rmSync(join(targetDir, "ui"), { recursive: true });
|
|
196
252
|
}
|
|
197
253
|
if (ui !== "vue")
|
|
@@ -199,14 +255,37 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
199
255
|
if (template !== "compose") {
|
|
200
256
|
// Only the capabilities change: the template's comments document the optional fields.
|
|
201
257
|
const path = join(targetDir, "dsm-package.yaml");
|
|
202
|
-
writeFileSync(path, setTopLevel(readFileSync(path, "utf8"), {
|
|
258
|
+
writeFileSync(path, setTopLevel(readFileSync(path, "utf8"), {
|
|
259
|
+
capabilities: nativeCapabilitySettings(caps),
|
|
260
|
+
}));
|
|
203
261
|
}
|
|
204
262
|
if (template === "compose" && options.from)
|
|
205
|
-
importCompose(options.from, targetDir
|
|
263
|
+
importCompose(options.from, targetDir);
|
|
206
264
|
if (template === "binary") {
|
|
207
265
|
mkdirSync(join(targetDir, "prebuilt"), { recursive: true });
|
|
208
|
-
for (const [arch, goarch] of [
|
|
209
|
-
|
|
266
|
+
for (const [arch, goarch] of [
|
|
267
|
+
["x86_64", "amd64"],
|
|
268
|
+
["aarch64", "arm64"],
|
|
269
|
+
]) {
|
|
270
|
+
const result = spawnSync("go", [
|
|
271
|
+
"build",
|
|
272
|
+
"-trimpath",
|
|
273
|
+
"-buildvcs=false",
|
|
274
|
+
"-ldflags",
|
|
275
|
+
"-s -w -buildid=",
|
|
276
|
+
"-o",
|
|
277
|
+
join(targetDir, "prebuilt", `api-${arch}`),
|
|
278
|
+
"main.go",
|
|
279
|
+
], {
|
|
280
|
+
cwd: join(targetDir, "example"),
|
|
281
|
+
env: goChildEnv({
|
|
282
|
+
...process.env,
|
|
283
|
+
CGO_ENABLED: "0",
|
|
284
|
+
GOOS: "linux",
|
|
285
|
+
GOARCH: goarch,
|
|
286
|
+
}),
|
|
287
|
+
encoding: "utf8",
|
|
288
|
+
});
|
|
210
289
|
if (result.error || result.status !== 0)
|
|
211
290
|
throw new Error(`[INIT_BINARY_BUILD_FAILED] Install Go and run init again, or provide your own static CGI binary. ${result.stderr ?? result.error}`);
|
|
212
291
|
}
|
|
@@ -218,15 +297,19 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
218
297
|
}
|
|
219
298
|
/** Each native template's method tests, as its README runs them; Node.js also compiles the backend/app the SPK ships. */
|
|
220
299
|
const BACKEND_TESTS = {
|
|
221
|
-
"native-go": "go test ./...",
|
|
222
|
-
"native-
|
|
300
|
+
"native-go": "go test ./...",
|
|
301
|
+
"native-python": "python3 -m unittest discover -s tests",
|
|
302
|
+
"native-node": "npm ci && npm test && npm run build",
|
|
303
|
+
"native-rust": "cargo test --locked",
|
|
223
304
|
};
|
|
224
305
|
/** What to run after init in `dir`, for a template built from source; undefined for the others. */
|
|
225
306
|
export function initNextSteps(template, dir, frontend, localSdk) {
|
|
226
307
|
if (!isNativeTemplate(template))
|
|
227
308
|
return undefined;
|
|
228
309
|
// --install-links packs a local SDK like the released package; a symlink would load the checkout's own node_modules.
|
|
229
|
-
const install = frontend
|
|
310
|
+
const install = frontend
|
|
311
|
+
? ` (cd frontend && npm install${localSdk ? " --install-links" : ""})\n`
|
|
312
|
+
: "";
|
|
230
313
|
return `Next steps:\n cd ${dir}\n (cd backend && ${BACKEND_TESTS[template]})\n${install} synopkgland build\nSee README.md.`;
|
|
231
314
|
}
|
|
232
315
|
/**
|
|
@@ -234,38 +317,74 @@ export function initNextSteps(template, dir, frontend, localSdk) {
|
|
|
234
317
|
* directory-derived) identity and keeps only the example install entries the
|
|
235
318
|
* imported Compose references as `${KEY}` (or `${KEY_UID}`/`${KEY_GID}`).
|
|
236
319
|
*/
|
|
237
|
-
function importCompose(from, targetDir
|
|
320
|
+
function importCompose(from, targetDir) {
|
|
238
321
|
const imported = readFileSync(from, "utf8");
|
|
239
322
|
const compose = parse(imported);
|
|
240
323
|
if (!compose || typeof compose !== "object" || !compose.services)
|
|
241
324
|
throw new Error("[INIT_COMPOSE_INVALID] --from needs a Compose YAML with services.");
|
|
242
|
-
|
|
243
|
-
const
|
|
325
|
+
const sourceRoot = dirname(resolve(from)), file = basename(from);
|
|
326
|
+
const envFile = existsSync(join(sourceRoot, ".env")) ? ".env" : undefined;
|
|
327
|
+
const inputs = composeInputs(sourceRoot, file, envFile);
|
|
328
|
+
for (const input of inputs) {
|
|
329
|
+
const dest = join(targetDir, input.path);
|
|
330
|
+
if (existsSync(dest) && input.path !== "compose.yaml")
|
|
331
|
+
throw new Error(`[INIT_TARGET_NOT_EMPTY] Imported input collides with scaffold: ${input.path}`);
|
|
332
|
+
}
|
|
333
|
+
if (file !== "compose.yaml")
|
|
334
|
+
rmSync(join(targetDir, "compose.yaml"));
|
|
335
|
+
for (const input of inputs) {
|
|
336
|
+
const dest = join(targetDir, input.path);
|
|
337
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
338
|
+
writeFileSync(dest, Buffer.from(input.content, "base64"), { mode: 0o600 });
|
|
339
|
+
}
|
|
340
|
+
// Import local sidecar input files without changing the upstream YAML. The
|
|
341
|
+
// SDK planner validates the complete include/env closure before any build.
|
|
342
|
+
const path = join(targetDir, "dsm-package.yaml");
|
|
244
343
|
const doc = parseDocument(readFileSync(path, "utf8"));
|
|
245
|
-
doc.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
344
|
+
doc.setIn(["compose", "files"], [file]);
|
|
345
|
+
if (envFile)
|
|
346
|
+
doc.setIn(["compose", "env_files"], [envFile]);
|
|
347
|
+
const services = Object.entries(compose.services);
|
|
348
|
+
const candidates = services
|
|
349
|
+
.flatMap(([service, spec]) => (spec.ports ?? []).map((port) => {
|
|
350
|
+
const target = typeof port === "object"
|
|
351
|
+
? Number(port.target)
|
|
352
|
+
: Number(String(port).split("/")[0].split(":").pop());
|
|
353
|
+
return { service, port: target };
|
|
354
|
+
}))
|
|
355
|
+
.filter((p) => Number.isInteger(p.port) && p.port > 0 && p.port < 65536);
|
|
356
|
+
if (candidates.length === 1)
|
|
357
|
+
doc.setIn(["compose", "web"], {
|
|
358
|
+
...candidates[0],
|
|
359
|
+
health: { path: "/", status: 200 },
|
|
360
|
+
});
|
|
361
|
+
else {
|
|
362
|
+
doc.deleteIn(["compose", "web"]);
|
|
363
|
+
doc.delete("identity_gateway");
|
|
364
|
+
} // Ambiguous ingress requires an explicit developer decision.
|
|
253
365
|
writeFileSync(path, String(doc));
|
|
254
366
|
}
|
|
255
367
|
function validDisplayName(displayName) {
|
|
256
368
|
if (displayName.trim() === "" || /[\r\n"\\]/.test(displayName)) {
|
|
257
|
-
throw new Error(
|
|
369
|
+
throw new Error('[INIT_DISPLAY_NAME_INVALID] The display name must be single-line and without " or \\.');
|
|
258
370
|
}
|
|
259
371
|
return displayName;
|
|
260
372
|
}
|
|
261
373
|
/** The `install.sharedFolder` block, validated by the same reader build-spk uses. */
|
|
262
374
|
function sharedFolderYaml(options) {
|
|
263
|
-
const folder = {
|
|
375
|
+
const folder = {
|
|
376
|
+
mode: options.mode,
|
|
377
|
+
key: options.key ?? "shared",
|
|
378
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
379
|
+
permission: options.permission ?? "rw",
|
|
380
|
+
};
|
|
264
381
|
const errors = [];
|
|
265
|
-
if (!readSharedFolder({ sharedFolder: folder }, e => errors.push(e.message))) {
|
|
382
|
+
if (!readSharedFolder({ sharedFolder: folder }, (e) => errors.push(e.message))) {
|
|
266
383
|
throw new Error(`[INIT_SHARED_FOLDER_INVALID] ${errors.join(" ")}`);
|
|
267
384
|
}
|
|
268
|
-
return `\ninstall:\n sharedFolder:\n${Object.entries(folder)
|
|
385
|
+
return `\ninstall:\n sharedFolder:\n${Object.entries(folder)
|
|
386
|
+
.map(([k, v]) => ` ${k}: ${v}\n`)
|
|
387
|
+
.join("")}`;
|
|
269
388
|
}
|
|
270
389
|
/**
|
|
271
390
|
* The App SDK a bridge App's tests import, in backend/vendor: the Python
|
|
@@ -276,7 +395,8 @@ function sharedFolderYaml(options) {
|
|
|
276
395
|
function vendorAppSdk(template, targetDir) {
|
|
277
396
|
if (template === "native-rust")
|
|
278
397
|
return;
|
|
279
|
-
const version = String(parse(readFileSync(join(targetDir, "dsm-package.yaml"), "utf8")).backend
|
|
398
|
+
const version = String(parse(readFileSync(join(targetDir, "dsm-package.yaml"), "utf8")).backend
|
|
399
|
+
.sdk);
|
|
280
400
|
const sdk = sdkSource(version).goDir;
|
|
281
401
|
const vendor = join(targetDir, "backend/vendor");
|
|
282
402
|
if (template === "native-python")
|
|
@@ -311,7 +431,7 @@ function useLocalSdk(targetDir, sdkDir) {
|
|
|
311
431
|
if (existsSync(sdkPackage)) {
|
|
312
432
|
const version = JSON.parse(readFileSync(sdkPackage, "utf8")).version;
|
|
313
433
|
if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version))
|
|
314
|
-
throw new Error(
|
|
434
|
+
throw new Error("[SDK_DIR_NOT_READY] Local SDK package version must be X.Y.Z.");
|
|
315
435
|
text = text.replace(/(github\.com\/synopackageland\/syno-pkg-land-sdk v)\d+\.\d+\.\d+\b/g, `$1${version}`);
|
|
316
436
|
}
|
|
317
437
|
writeFileSync(goMod, `${text}\nreplace ${SDK_MODULE} => ${sdkDir}\n`);
|
|
@@ -363,5 +483,9 @@ function substitutionsFor(template, targetDir, options) {
|
|
|
363
483
|
throw new Error(`[INIT_PACKAGE_INVALID] package "${packageId}" must start with a letter and contain only alphanumerics (2-64 characters), for example PhotoTagger.`);
|
|
364
484
|
}
|
|
365
485
|
const displayName = validDisplayName(options.displayName ?? defaultDisplayName(targetDir));
|
|
366
|
-
return {
|
|
486
|
+
return {
|
|
487
|
+
__PACKAGE__: packageId,
|
|
488
|
+
__DISPLAY_NAME__: displayName,
|
|
489
|
+
__BACKGROUND_ENABLED__: caps.includes("jobs") || caps.includes("delegate") ? "true" : "false",
|
|
490
|
+
};
|
|
367
491
|
}
|
package/dist/inspect.js
CHANGED
|
@@ -18,7 +18,14 @@ export function inspectProject(project) {
|
|
|
18
18
|
}
|
|
19
19
|
const parsed = loadCompose(project.composePath);
|
|
20
20
|
if (!parsed) {
|
|
21
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
services,
|
|
23
|
+
publishedPorts,
|
|
24
|
+
dataMounts,
|
|
25
|
+
risks,
|
|
26
|
+
webHints,
|
|
27
|
+
exposedPorts: [],
|
|
28
|
+
};
|
|
22
29
|
}
|
|
23
30
|
const compose = parsed.compose;
|
|
24
31
|
services.push(...listServiceNames(compose));
|
|
@@ -29,12 +36,19 @@ export function inspectProject(project) {
|
|
|
29
36
|
collectRisks(compose, risks);
|
|
30
37
|
if (publishedPorts.length > 0) {
|
|
31
38
|
const primary = publishedPorts[0];
|
|
32
|
-
webHints.push(`Detected published port ${primary.port};
|
|
33
|
-
webHints.push("
|
|
34
|
-
webHints.push("Launch Mode:
|
|
39
|
+
webHints.push(`Detected published port ${primary.port}; declare the web service/target for no-publish ingress in dsm-package.yaml compose.web.`);
|
|
40
|
+
webHints.push("Use the SDK identity gateway for Web ingress and review its access policy.");
|
|
41
|
+
webHints.push("Launch Mode: ui.launch is dsm-window or new-page (Compose default, new tab).");
|
|
35
42
|
}
|
|
36
43
|
const exposedPorts = listPortExposures(compose).filter((exposure) => !exposure.loopbackOnly);
|
|
37
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
services,
|
|
46
|
+
publishedPorts,
|
|
47
|
+
dataMounts,
|
|
48
|
+
risks,
|
|
49
|
+
webHints,
|
|
50
|
+
exposedPorts,
|
|
51
|
+
};
|
|
38
52
|
}
|
|
39
53
|
function collectDataMounts(compose, out) {
|
|
40
54
|
if (!compose.services) {
|
|
@@ -43,7 +57,8 @@ function collectDataMounts(compose, out) {
|
|
|
43
57
|
for (const [serviceName, service] of Object.entries(compose.services)) {
|
|
44
58
|
const volumes = normalizeVolumeEntries(service.volumes);
|
|
45
59
|
for (const volume of volumes) {
|
|
46
|
-
if (volume.includes("SYNOPKG_PKGVAR") ||
|
|
60
|
+
if (volume.includes("SYNOPKG_PKGVAR") ||
|
|
61
|
+
volume.includes("SYNO_APP_SECRETS_DIR")) {
|
|
47
62
|
out.push(`${serviceName}: ${volume}`);
|
|
48
63
|
}
|
|
49
64
|
}
|
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"0.
|
|
3
|
-
"x86_64": "efe409287770f6b70e7f8465e8767eeb2413b9ab8775020cf4dffb1555d7bf1f",
|
|
4
|
-
"aarch64": "d68c9889ed2391e918b9c278679ea0a01cd04425528c11090700a3b6341fc543",
|
|
5
|
-
"armv7": "9149a05efb9f2d7d86a400707099b3d657382b1c0dbad6588be8c500e90445cc",
|
|
6
|
-
"lifecycle": {
|
|
7
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
8
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
9
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
10
|
-
},
|
|
11
|
-
"goToolchain": "go1.26.9"
|
|
12
|
-
},
|
|
13
|
-
"0.14.0": {
|
|
2
|
+
"0.15.0": {
|
|
14
3
|
"x86_64": "efe409287770f6b70e7f8465e8767eeb2413b9ab8775020cf4dffb1555d7bf1f",
|
|
15
4
|
"aarch64": "d68c9889ed2391e918b9c278679ea0a01cd04425528c11090700a3b6341fc543",
|
|
16
5
|
"armv7": "9149a05efb9f2d7d86a400707099b3d657382b1c0dbad6588be8c500e90445cc",
|
package/dist/skill.js
CHANGED
|
@@ -3,17 +3,59 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
const SKILL_CATALOG = [
|
|
5
5
|
{ id: "orchestrator", file: "SKILL.md", when: "Read first" },
|
|
6
|
-
{ id: "
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
{ id: "ctl", file: "reference/ctl.md", when: "Operate registered NAS Apps or design AI commands" },
|
|
7
|
+
{
|
|
8
|
+
id: "create",
|
|
9
|
+
file: "reference/create.md",
|
|
10
|
+
when: "Start from nothing or a one-liner",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "compose",
|
|
14
|
+
file: "reference/compose.md",
|
|
15
|
+
when: "Upstream Compose with the SDK planner, fixed identity and support boundary",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "wrap",
|
|
19
|
+
file: "reference/wrap.md",
|
|
20
|
+
when: "Existing Compose, or adding a contract",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "identity-gateway",
|
|
24
|
+
file: "reference/identity-gateway.md",
|
|
25
|
+
when: "Wrap OSS native or container apps with the DSM identity gateway",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "identity",
|
|
29
|
+
file: "reference/identity.md",
|
|
30
|
+
when: "Needs a DSM sign-in",
|
|
31
|
+
},
|
|
10
32
|
{ id: "files", file: "reference/files.md", when: "Needs NAS files" },
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
{
|
|
34
|
+
id: "portal",
|
|
35
|
+
file: "reference/portal.md",
|
|
36
|
+
when: "Needs a DSM Application Portal alias link",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "ui-vue",
|
|
40
|
+
file: "reference/ui-vue.md",
|
|
41
|
+
when: "Write or check a Vue UI",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "ui-json",
|
|
45
|
+
file: "reference/ui-json.md",
|
|
46
|
+
when: "Generate or check a JSON UI",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "lifecycle",
|
|
50
|
+
file: "reference/lifecycle.md",
|
|
51
|
+
when: "Add work to install, upgrade, uninstall, start or stop (dsm-scripts/)",
|
|
52
|
+
},
|
|
15
53
|
{ id: "deploy", file: "reference/deploy.md", when: "Deploy to a NAS" },
|
|
16
|
-
{
|
|
54
|
+
{
|
|
55
|
+
id: "diagnose",
|
|
56
|
+
file: "reference/diagnose.md",
|
|
57
|
+
when: "check/deploy/Open failed",
|
|
58
|
+
},
|
|
17
59
|
];
|
|
18
60
|
export function resolveSkillsRoot() {
|
|
19
61
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { validateDescriptor } from '../ctl/descriptor.js';
|
|
2
|
+
export function inspectApiDescription(raw, info, expectedHash, registry) {
|
|
3
|
+
try {
|
|
4
|
+
const description = validateDescriptor(JSON.parse(raw), info.id, info.version);
|
|
5
|
+
if (registry && Object.entries(description.payload.bindings).some(([method, binding]) => !Object.hasOwn(registry, method) || registry[method] !== binding.permission))
|
|
6
|
+
return ['CTL_COMMAND_BINDING_MISMATCH command method/permission differs from build registry.'];
|
|
7
|
+
return description.sha256 === expectedHash ? [] : ['CTL_DESCRIPTOR_HASH_MISMATCH API description differs from the build record.'];
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return ['CTL_DESCRIPTOR_INVALID API description does not match the schema, commands or final INFO.'];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Descriptor } from '../ctl/descriptor.js';
|
|
2
|
+
import { type StandaloneManifest } from './manifest.js';
|
|
3
|
+
import type { CommandRunner } from './build.js';
|
|
4
|
+
export declare function buildApiDescription(m: StandaloneManifest, bridgeMethods: string[] | undefined, run: CommandRunner): Promise<{
|
|
5
|
+
description: Descriptor;
|
|
6
|
+
registry: Record<string, string>;
|
|
7
|
+
} | undefined>;
|