@synopackageland/templates 0.38.0 → 0.39.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.
Files changed (62) hide show
  1. package/README.md +9 -9
  2. package/package.json +1 -1
  3. package/templates/_shared/AGENTS.md +6 -5
  4. package/templates/_shared/CLAUDE.md +2 -0
  5. package/templates/compose/dsm-package.yaml +1 -1
  6. package/templates/native-go/README.md +28 -11
  7. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  8. package/templates/native-go/backend/cmd/app/main.go +9 -2
  9. package/templates/native-go/backend/go.mod +1 -1
  10. package/templates/native-go/backend/go.sum +2 -2
  11. package/templates/native-go/backend/internal/api/operations.json +5 -0
  12. package/templates/native-go/backend/internal/api/permissions.go +7 -0
  13. package/templates/native-go/backend/internal/api/permissions.json +3 -1
  14. package/templates/native-go/backend/internal/app/routes.go +2 -46
  15. package/templates/native-go/backend/internal/app/routes_test.go +4 -25
  16. package/templates/native-go/dsm-package.yaml +2 -28
  17. package/templates/native-go/examples/operations.json +112 -0
  18. package/templates/native-go/operations.json +27 -0
  19. package/templates/native-go/ui/app.js +0 -25
  20. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -4
  21. package/templates/native-go-root-ops/frontend/test/executor-gate.test.ts +7 -6
  22. package/templates/native-go-vue/frontend/package-lock.json +4 -4
  23. package/templates/native-go-vue/frontend/package.json +1 -1
  24. package/templates/native-go-vue/frontend/src/App.vue +5 -34
  25. package/templates/native-go-vue/frontend/test/apps.test.ts +19 -25
  26. package/templates/native-go-vue/frontend/test/native-controls.test.ts +4 -1
  27. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +5 -3
  28. package/templates/native-node/README.md +27 -7
  29. package/templates/native-node/backend/app/permissions.json +3 -1
  30. package/templates/native-node/backend/package-lock.json +1 -1
  31. package/templates/native-node/backend/src/routes.ts +5 -43
  32. package/templates/native-node/backend/test/routes.test.ts +3 -23
  33. package/templates/native-node/dsm-package.yaml +3 -29
  34. package/templates/native-node/examples/operations.json +112 -0
  35. package/templates/native-node/operations.json +27 -0
  36. package/templates/native-node-delegate/operations.patch.json +20 -0
  37. package/templates/native-node-jobs/operations.patch.json +52 -0
  38. package/templates/native-node-root-ops/operations.patch.json +20 -0
  39. package/templates/native-python/README.md +27 -7
  40. package/templates/native-python/backend/app/permissions.json +3 -1
  41. package/templates/native-python/backend/app/routes.py +2 -41
  42. package/templates/native-python/backend/tests/test_routes.py +3 -19
  43. package/templates/native-python/dsm-package.yaml +3 -29
  44. package/templates/native-python/examples/operations.json +112 -0
  45. package/templates/native-python/operations.json +27 -0
  46. package/templates/native-python-delegate/operations.patch.json +20 -0
  47. package/templates/native-python-jobs/operations.patch.json +52 -0
  48. package/templates/native-python-root-ops/operations.patch.json +20 -0
  49. package/templates/native-rust/README.md +28 -8
  50. package/templates/native-rust/backend/Cargo.lock +2 -2
  51. package/templates/native-rust/backend/Cargo.toml +1 -1
  52. package/templates/native-rust/backend/permissions.json +3 -1
  53. package/templates/native-rust/backend/src/routes.rs +11 -149
  54. package/templates/native-rust/dsm-package.yaml +3 -29
  55. package/templates/native-rust/examples/operations.json +112 -0
  56. package/templates/native-rust/operations.json +27 -0
  57. package/templates/native-rust-delegate/backend/Cargo.lock +2 -2
  58. package/templates/native-rust-delegate/backend/Cargo.toml +1 -1
  59. package/templates/native-rust-delegate/backend/src/delegate.rs +7 -1
  60. package/templates/native-rust-delegate/operations.patch.json +20 -0
  61. package/templates/native-rust-jobs/operations.patch.json +52 -0
  62. package/templates/native-rust-root-ops/operations.patch.json +20 -0
package/README.md CHANGED
@@ -22,7 +22,7 @@ daemon:
22
22
  window_seconds: 300
23
23
  ```
24
24
 
25
- Templates 0.38.0 pin UI 0.19.1 and SDK 0.43.0 in native and Compose projects.
25
+ Templates 0.38.0 pin UI 0.19.1 and SDK 0.47.0 in native and Compose projects.
26
26
  Use a CLI that supports executor manifest format 1. The Vue `AppShell` shows a
27
27
  non-blocking **Reload** notice when an upgrade replaced the UI an open page
28
28
  loaded. Go Apps select
@@ -87,16 +87,15 @@ the single source of the SDK version and `frontend/package.json` pins
87
87
  `@synopackageland/sdk` to the same version. Python and Node.js name it in
88
88
  `backend.sdk`, Rust in the `syno-pkg-land-app` tag of `backend/Cargo.toml`
89
89
  (and `Cargo.lock`); all of them pin 0.41.1. The Vue frontend pins
90
- `@synopackageland/ui` 0.19.1. SDK 0.43.0 accepts UI `>=0.10.1 <0.20.0`,
90
+ `@synopackageland/ui` 0.19.1. SDK 0.47.0 accepts UI `>=0.10.1 <0.20.0`,
91
91
  so no UI peer override is needed. Tests use jsdom 30 for UI floating-layer styles.
92
92
 
93
93
  Interactive NAS file operations use same-origin browser calls.
94
94
  Custom file handlers use the SDK's
95
95
  [session transport migration](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.41.1/docs/dsm-session-transport.md).
96
- The CLI reads SDK 0.43.0's release manifest, which names immutable
96
+ The CLI reads SDK 0.47.0's release manifest, which names immutable
97
97
  executor/v0.39.0 and its executor digests. CLI distributions must resolve
98
- templates 0.38.x before default scaffolds use this release; a `~0.36.0`
99
- templates dependency remains on the older minor.
98
+ templates 0.39.x for the current registry scaffolds.
100
99
 
101
100
  Every App method must appear in the permissions table, including methods whose
102
101
  permission equals `default: administrators`. Dynamic method names and custom
@@ -110,7 +109,7 @@ Keep punctuation between sentences and intentional loading ellipses.
110
109
 
111
110
  ## Request deadlines and background work
112
111
 
113
- SDK/App 0.43.0 defaults to 300 seconds for cooperative requests and 302 seconds
112
+ SDK/App 0.47.0 defaults to 300 seconds for cooperative requests and 302 seconds
114
113
  for the TS client and generated DSM window. The CLI reads the selected SDK's
115
114
  timeout spec; `backend.timeout_seconds` explicitly overrides it for native
116
115
  backends, and an earlier parent deadline still wins. Go assembly should fall
@@ -304,7 +303,7 @@ The bridge examples expose submission, lookup and cancellation through
304
303
 
305
304
  ## External access
306
305
 
307
- With a compatible CLI and SDK 0.43.0, a gateway App may declare `identity_gateway.external_access: {connect: optional, origin_change: confirm, single_origin: true}` for an upstream that signs in on one fixed URL. The policy is forwarded to the packaged gateway. A single-origin App's `manual_origin` accepts an explicit HTTPS port; the `external-origin` installation setting remains restricted to port 443. The DSM launcher selects a provider destination, probes `/_spl/ping` for up to eight seconds, and explains failures before offering **Open anyway**. A configured single-origin App always opens its primary external origin.
306
+ With a compatible CLI and SDK 0.47.0, a gateway App may declare `identity_gateway.external_access: {connect: optional, origin_change: confirm, single_origin: true}` for an upstream that signs in on one fixed URL. The policy is forwarded to the packaged gateway. A single-origin App's `manual_origin` accepts an explicit HTTPS port; the `external-origin` installation setting remains restricted to port 443. The DSM launcher selects a provider destination, probes `/_spl/ping` for up to eight seconds, and explains failures before offering **Open anyway**. A configured single-origin App always opens its primary external origin.
308
307
 
309
308
 
310
309
  Compose templates declare an app-named dedicated shared folder by default. DSM
@@ -351,5 +350,6 @@ for retry because the executor may already have admitted it. Other settled
351
350
  sources use the SDK's explicit discard operation when the App deletes the job.
352
351
 
353
352
  An opt-in [DSM path ingress migration guide](examples/path-ingress/README.md)
354
- explains how a prefix-aware upstream uses `/synopkgland/<package>/` on the DSM
355
- origin after upgrading to a matching CLI and SDK release.
353
+ explains the default `/synopkgland/<package>/` on the DSM origin and opt-in
354
+ custom lowercase paths for upstreams with a fixed base. Root `/` is unsupported;
355
+ installation checks route ownership before DSM registers the resource.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/templates",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Project templates for synopkgland init: native-go, native-python, native-node and native-rust (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,13 +54,15 @@ Application Privileges allows for `SYNO.SDS.<package>.Instance`; DSM's install
54
54
  default allows administrators only, so nobody else enters until an admin grants
55
55
  access. One CGI check covers every route; failures refuse; changes apply within
56
56
  30 seconds. Never re-implement AppPriv rules in the App.
57
- List every App method explicitly in permissions.json, including administrator
58
- methods. Dynamic method names and unregistered HTTP dispatchers are rejected; use
57
+ Declare every App method once in operations.json with a required ai class. Run
58
+ `ctl generate` to generate permissions, routes, commands, AI tools and the App
59
+ reference; run `ctl generate --check` and `check` to refuse drift. Read generated
60
+ operation descriptions instead of maintaining a second list in a skill. Dynamic method names and unregistered HTTP dispatchers are rejected; use
59
61
  fixed SDK route constructors. Use identity_gateway.upstream_auth_paths for
60
62
  client API paths authenticated by the upstream.
61
63
  `api.HandleHTTP("/proxy/", "app.proxy", handler)` serves raw HTTP
62
64
  for literal, disjoint full-segment prefixes outside SDK reserved paths. Declare
63
- the key as authenticated or administrators in permissions.json. Origin, entry,
65
+ the key as authenticated or administrators in operations.json, then generate. Origin, entry,
64
66
  identity and permission checks precede raw streaming/initial-upgrade handlers;
65
67
  CGI itself cannot upgrade a connection. For package-private worker HTTP use
66
68
  `ipc.ServeUnix(ctx, path, handler)` and `ipc.DialUnix`: owned mode-0600 sockets
@@ -76,8 +78,7 @@ dsm-package.yaml.
76
78
  Mark routes ordinary users call `authenticated`, mark whole admin routes
77
79
  `administrators`, guard global settings inside shared routes with
78
80
  the SDK's require-admin check, and hide admin controls with `ctx.user?.isAdmin`
79
- (UI only, never the authority). Example: users read the greeting; only admins
80
- call `greeting.set`.
81
+ (UI only, never the authority). Declare actual App permissions in operations.json and regenerate the policy.
81
82
 
82
83
  ## DSM Application Portal (`portal`)
83
84
 
@@ -1 +1,3 @@
1
1
  Read [AGENTS.md](AGENTS.md).
2
+
3
+ Operation metadata belongs only in operations.json; read the generated reference. Run ctl generate, ctl generate --check and check.
@@ -23,7 +23,7 @@ compose:
23
23
  web: {service: web, port: 8080, health: {path: /, status: 200}}
24
24
  network: {egress: isolated}
25
25
  identity_gateway:
26
- sdk: 0.43.0
26
+ sdk: 0.47.0
27
27
  mode: none
28
28
  listen: {http: 28000}
29
29
  # Web Apps default to new-page through the shared app-launch resolver.
@@ -6,7 +6,7 @@ runs as its own DSM package account, needs no containers, and has no root by def
6
6
 
7
7
  ```text
8
8
  DSM main menu → DSM window (ui/app.js)
9
- │ ctx.call("greeting.get") same-origin POST + DSM session
9
+ │ ctx.call("app.status") same-origin POST + DSM session
10
10
  ▼
11
11
  /webman/3rdparty/__PACKAGE__/cgi/api.cgi (backend/, runs as package user)
12
12
  1. transport guards: POST, JSON, same origin, ≤ 1 MiB, bounded JSON complexity
@@ -17,7 +17,7 @@ DSM main menu → DSM window (ui/app.js)
17
17
 
18
18
  ## Requests and long work
19
19
 
20
- SDK/App 0.43.0 defaults to 300-second requests and a 302-second browser wait.
20
+ SDK/App 0.47.0 defaults to 300-second requests and a 302-second browser wait.
21
21
  Explicit `backend.timeout_seconds` and earlier parent deadlines still apply;
22
22
  rebuild the App to use the new SDK. For long NAS work, enable the existing jobs
23
23
  capability: return admission and the original job ID promptly, execute in the
@@ -27,7 +27,7 @@ last queued/running state intact. After a lost admission reply, reuse the same
27
27
  idempotency key and input instead of replaying a mutation with a new key.
28
28
  Jobs default to 30 minutes; approved executor limits remain separate. ctl keeps
29
29
  its 62-second transport default; use an explicit `--timeout` or an existing
30
- nonblocking job. See the SDK's [request deadline contract](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.43.0/docs/request-deadlines.md).
30
+ nonblocking job. See the SDK's [request deadline contract](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.47.0/docs/request-deadlines.md).
31
31
 
32
32
  ## Quick start
33
33
 
@@ -35,6 +35,8 @@ Needs Node.js with `synopkgland` installed, and Go 1.25+.
35
35
 
36
36
  ```bash
37
37
  (cd backend && go test ./...) # auth policy + route tests
38
+ synopkgland ctl generate
39
+ synopkgland check
38
40
  synopkgland build # → dist/__PACKAGE__-0.1.0-0001-{x86_64,aarch64}.spk
39
41
  synopkgland inspect dist/__PACKAGE__-0.1.0-0001-x86_64.spk
40
42
  ```
@@ -42,8 +44,7 @@ synopkgland inspect dist/__PACKAGE__-0.1.0-0001-x86_64.spk
42
44
  Install: Package Center → Settings → General → Trust Level "Any publisher",
43
45
  then Manual Install with the SPK for your NAS's CPU. Open
44
46
  **__DISPLAY_NAME__** from the DSM main menu: it shows who you are signed in
45
- as, what the backend reports, and (for administrators) a form that stores a
46
- greeting in the package's data folder.
47
+ as and what the backend reports.
47
48
 
48
49
  ## Capabilities and schema 2
49
50
 
@@ -79,7 +80,7 @@ resolve them before retrying. Already-declared capabilities are a no-op.
79
80
  | `i18n/messages/<locale>.json` | UI text in nine languages | Yes |
80
81
  | `i18n/glossary.json` | DSM's own UI terms, and which messages must use them | Add terms |
81
82
  | `backend/internal/app/` | Your API: routes and handlers | Yes — your backend |
82
- | `backend/internal/api/permissions.json` | Who may call each of your methods | Yes |
83
+ | `backend/internal/api/permissions.json` | Generated method policy | No |
83
84
  | `backend/cmd/app/` | Wires your routes to the SDK server | Rarely |
84
85
  | `backend/go.mod` | Your module; `require github.com/synopackageland/syno-pkg-land-sdk` pins the SDK | SDK upgrades |
85
86
  | `dist/` | Built SPKs (ignored by git) | No |
@@ -91,15 +92,13 @@ resolve them before retrying. Already-declared capabilities are a no-op.
91
92
  ## Add an API operation
92
93
 
93
94
  Add a handler in `backend/internal/app/routes.go`. Every method defaults to
94
- DSM administrator membership, resolved by its native SDK. All access rules live in
95
- `backend/internal/api/permissions.json`, including launcher access:
95
+ DSM administrator membership, resolved by its native SDK. Operation metadata and access rules live in `operations.json`;
96
+ `ctl generate` writes the runtime policy and launcher access:
96
97
 
97
98
  ```go
98
99
  func Routes(cfg Config) []api.Route {
99
100
  return []api.Route{
100
101
  api.Handle("app.status", cfg.status),
101
- api.Handle("greeting.get", cfg.getGreeting),
102
- api.Handle("greeting.set", cfg.setGreeting),
103
102
  api.Handle("notes.list", cfg.listNotes), // new: administrators by default
104
103
  api.Handle("notes.purge", cfg.purgeNotes), // new: administrators by default
105
104
  }
@@ -397,7 +396,7 @@ Use `install.sharedFolder: {mode: dedicated, key: media, name: __PACKAGE__, perm
397
396
  for shared data. DSM creates the app-specific folder and grants the declared
398
397
  package-account permission. The wizard defaults to it and validates any existing
399
398
  folder choice before Next. The backend reports the real state: the `sharedfolder.status` operation (administrators only
400
- unless you change `permissions.json`) returns each folder with what the App needs, what the
399
+ unless you change the operation registry) returns each folder with what the App needs, what the
401
400
  package account has (`none`, `read`, `write`) and a `state` (`ready`, `pending`, `missing`,
402
401
  `ambiguous`). `cfg.SharedFolderPath(key)` returns the path or an error that names the
403
402
  Control Panel step instead of failing on a file read. Keep `SharedFolderRequired` in
@@ -426,3 +425,21 @@ Submit with `bg.jobs.submit`; use `bg.jobs.get` for progress and
426
425
  `bg.jobs.cancel` to cancel. Pause/resume and existing schedule routes continue
427
426
  to operate on retained imported copies. Schedules require explicit consent and
428
427
  reprocess the fixed private copy, without rereading the source file.
428
+
429
+ ## Operation registry
430
+
431
+ Declare each App method once in `operations.json`, including its English summary,
432
+ params/result schemas, effect, permission, flow and required `ai` class. The
433
+ SDK contributes its own hidden operation catalog for enabled capability routes.
434
+ Register the handler, then run `synopkgland ctl generate`, `synopkgland ctl
435
+ generate --check` and `synopkgland check`. Build refuses generated drift or a
436
+ route/spec mismatch. Never maintain commands or operation descriptions in the
437
+ manifest or skill by hand. Read the generated `generated/operations.md`.
438
+
439
+ `examples/operations.json` illustrates read, preview, draft, human-only and hidden.
440
+ It is documentation only: its operations have no handlers and are never shipped
441
+ as commands. Copy a declaration only when implementing a real App capability.
442
+ Draft writes require a non-mutating preview and explicit human confirmation.
443
+ Long operations use the existing job flow; tools own polling and pagination.
444
+
445
+ Use a matching SDK/CLI/templates release containing the registry support.
@@ -1,6 +1,6 @@
1
1
  Native Go package build dependencies
2
2
 
3
- github.com/synopackageland/syno-pkg-land-sdk v0.43.0 (MIT)
3
+ github.com/synopackageland/syno-pkg-land-sdk v0.47.0 (MIT)
4
4
 
5
5
  Copyright (c) 2026 synopkgland
6
6
 
@@ -47,6 +47,8 @@ var (
47
47
  portal = ""
48
48
  // backend.timeout_seconds, api.DefaultRequestTimeout unless set.
49
49
  requestTimeout = ""
50
+ // Build enables registry equality only for Apps that opt in to ctl.registry.
51
+ operationRegistry = ""
50
52
  )
51
53
 
52
54
  // deadline is the CGI request deadline: requestTimeout seconds, or
@@ -73,7 +75,7 @@ func main() {
73
75
  if err != nil {
74
76
  log.Fatal(err)
75
77
  }
76
- cfg := app.Config{PackageID: packageID, Version: version, DataDir: ".", Portal: portal != ""}
78
+ cfg := app.Config{PackageID: packageID, Version: version, DataDir: ".", Portal: portal != "", PortalExecutor: portal == "executor"}
77
79
  if app.SessionFilesEnabled(fileStation) {
78
80
  cfg.SessionFiles = filestation.Local(cfg.DataDir)
79
81
  }
@@ -131,7 +133,12 @@ func main() {
131
133
  if err != nil {
132
134
  log.Fatal(err)
133
135
  }
134
- srv, err := api.NewServerWithPermissions(auth, permissions, app.Routes(cfg)...)
136
+ var srv *api.Server
137
+ if operationRegistry == "on" {
138
+ srv, err = api.NewServerWithOperations(auth, appapi.Operations(), permissions, app.Routes(cfg)...)
139
+ } else {
140
+ srv, err = api.NewServerWithPermissions(auth, permissions, app.Routes(cfg)...)
141
+ }
135
142
  if err != nil {
136
143
  log.Fatal(err)
137
144
  }
@@ -4,6 +4,6 @@ go 1.25.0
4
4
 
5
5
  toolchain go1.26.10
6
6
 
7
- require github.com/synopackageland/syno-pkg-land-sdk v0.43.0
7
+ require github.com/synopackageland/syno-pkg-land-sdk v0.47.0
8
8
 
9
9
  require golang.org/x/sys v0.44.0 // indirect
@@ -1,4 +1,4 @@
1
- github.com/synopackageland/syno-pkg-land-sdk v0.43.0 h1:SgUaOxjyOeiZE44Z+JAmZXtoy+idkgVF74N7palFH4k=
2
- github.com/synopackageland/syno-pkg-land-sdk v0.43.0/go.mod h1:8GwREGbczc8mAk4Gv8FUHaukXXCUstjzkovQrNV59Cc=
1
+ github.com/synopackageland/syno-pkg-land-sdk v0.47.0 h1:NYGAo/D1MTuC/rfHe61JpVdgBmiY4DW5C0AvNvNIVxU=
2
+ github.com/synopackageland/syno-pkg-land-sdk v0.47.0/go.mod h1:8GwREGbczc8mAk4Gv8FUHaukXXCUstjzkovQrNV59Cc=
3
3
  golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
4
4
  golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
@@ -0,0 +1,5 @@
1
+ {
2
+ "format": "synopkgland.api",
3
+ "version": 2,
4
+ "operations": {}
5
+ }
@@ -24,3 +24,10 @@ func Permissions() ([]byte, error) {
24
24
  }
25
25
  return permissionsJSON, nil
26
26
  }
27
+
28
+ // operationsJSON is generated from the App registry and the SDK capability catalog.
29
+ //
30
+ //go:embed operations.json
31
+ var operationsJSON []byte
32
+
33
+ func Operations() []byte { return operationsJSON }
@@ -1,5 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "default": "administrators",
4
- "methods": {}
4
+ "methods": {
5
+ "app.status": "administrators"
6
+ }
5
7
  }
@@ -1,7 +1,5 @@
1
- // Package app holds this package's business operations. Add a route here and
2
- // register its handler here. App permissions live in internal/api/permissions.json; SDK capability routes
3
- // use the immutable SDK policy.
4
- // unlisted methods require membership in DSM's administrators group.
1
+ // Package app holds the business handlers. Declare each operation once in
2
+ // operations.json; ctl generate writes the policy and SDK runtime registry.
5
3
  //
6
4
  //line __PACKAGE__/internal/app/routes.go:1
7
5
  package app
@@ -10,13 +8,10 @@ import (
10
8
  "bytes"
11
9
  "context"
12
10
  "encoding/json"
13
- "errors"
14
- "io/fs"
15
11
  "os"
16
12
  "path/filepath"
17
13
  "runtime"
18
14
  "time"
19
- "unicode/utf8"
20
15
 
21
16
  "github.com/synopackageland/syno-pkg-land-sdk/api"
22
17
  "github.com/synopackageland/syno-pkg-land-sdk/filestation"
@@ -59,8 +54,6 @@ func Routes(cfg Config) []api.Route {
59
54
  routes = append(routes, []api.Route{
60
55
  api.Handle("app.status", cfg.status),
61
56
  api.Handle("sharedfolder.status", cfg.sharedFolderStatus),
62
- api.Handle("greeting.get", cfg.getGreeting),
63
- api.Handle("greeting.set", cfg.setGreeting),
64
57
  }...)
65
58
  routes = append(routes, cfg.SessionFiles.Routes()...)
66
59
  routes = append(routes, cfg.backgroundRoutes()...)
@@ -81,43 +74,6 @@ func (cfg Config) status(_ context.Context, caller api.Caller, _ json.RawMessage
81
74
  }, nil
82
75
  }
83
76
 
84
- type greeting struct {
85
- Text string `json:"text"`
86
- UpdatedBy string `json:"updated_by,omitempty"`
87
- }
88
-
89
- func (cfg Config) greetingPath() string { return filepath.Join(cfg.DataDir, "greeting.json") }
90
-
91
- func (cfg Config) getGreeting(context.Context, api.Caller, json.RawMessage) (any, error) {
92
- data, err := os.ReadFile(cfg.greetingPath())
93
- if errors.Is(err, fs.ErrNotExist) {
94
- return greeting{Text: "Hello"}, nil
95
- }
96
- if err != nil {
97
- return nil, err
98
- }
99
- var g greeting
100
- return g, json.Unmarshal(data, &g)
101
- }
102
-
103
- func (cfg Config) setGreeting(_ context.Context, caller api.Caller, params json.RawMessage) (any, error) {
104
- var in struct {
105
- Text string `json:"text"`
106
- }
107
- if err := decodeParams(params, &in); err != nil {
108
- return nil, err
109
- }
110
- if n := utf8.RuneCountInString(in.Text); n < 1 || n > 200 {
111
- return nil, api.BadRequest("text must be 1 to 200 characters")
112
- }
113
- g := greeting{Text: in.Text, UpdatedBy: caller.User()}
114
- data, err := json.Marshal(g)
115
- if err != nil {
116
- return nil, err
117
- }
118
- return g, writeFileAtomic(cfg.greetingPath(), data)
119
- }
120
-
121
77
  // decodeParams decodes params strictly: unknown fields are an error.
122
78
  func decodeParams(params json.RawMessage, into any) error {
123
79
  if len(params) == 0 {
@@ -109,8 +109,6 @@ var sampleOutcome = map[string]string{}
109
109
  var sampleParams = map[string]string{
110
110
  "app.status": `{}`,
111
111
  "sharedfolder.status": `{}`,
112
- "greeting.get": `{}`,
113
- "greeting.set": `{"text":"Hi"}`,
114
112
  }
115
113
 
116
114
  func TestEveryRouteHasSampleParamsAndNoneIsPublic(t *testing.T) {
@@ -174,30 +172,11 @@ func TestStatusReportsPackageAndCaller(t *testing.T) {
174
172
  }
175
173
  }
176
174
 
177
- func TestGreetingDefaultsAndAdminCanChangeIt(t *testing.T) {
178
- cfg := testConfig(t)
179
- _, out := call(t, cfg, alice, "greeting.get", `{}`)
180
- if got := out["data"].(map[string]any)["text"]; got != "Hello" {
181
- t.Fatalf("default greeting = %v", got)
182
- }
183
- if code, out := call(t, cfg, alice, "greeting.set", `{"text":"Hi from DSM"}`); code != "" {
184
- t.Fatalf("set: %q %v", code, out)
185
- }
186
- _, out = call(t, cfg, alice, "greeting.get", `{}`)
187
- data := out["data"].(map[string]any)
188
- if data["text"] != "Hi from DSM" || data["updated_by"] != "alice" {
189
- t.Fatalf("after set: %v", data)
190
- }
191
- }
192
-
193
- func TestGreetingSetValidatesText(t *testing.T) {
194
- for _, params := range []string{`{}`, `{"text":""}`, `{"text":"` + strings.Repeat("x", 201) + `"}`, `{"text":7}`, `{"text":"ok","extra":1}`} {
195
- cfg := testConfig(t)
196
- code, _ := call(t, cfg, alice, "greeting.set", params)
197
- if code != "bad_request" {
198
- t.Errorf("%s: %q, want bad_request", params, code)
175
+ func TestSampleOperationsAreAbsent(t *testing.T) {
176
+ for _, method := range []string{"greeting.get", "greeting.set"} {
177
+ if code, _ := call(t, testConfig(t), alice, method, `{}`); code != "unknown_method" {
178
+ t.Fatalf("sample route %s remains: %s", method, code)
199
179
  }
200
- assertDataDirEmpty(t, cfg)
201
180
  }
202
181
  }
203
182
 
@@ -103,33 +103,7 @@ capabilities: {}
103
103
  # items:
104
104
  # - key: jobs # i18n key uninstall.state.jobs
105
105
 
106
- # Explicit AI commands. Internal/UI methods are not automatically exposed.
106
+ # Operation metadata has one source. Generate/check before building.
107
107
  ctl:
108
108
  enabled: true
109
- commands:
110
- status.show:
111
- cli: [status]
112
- summary: Show this App's status and verified caller
113
- effect: read
114
- flow: {type: invoke, method: app.status}
115
- paramsSchema: {type: object, additionalProperties: false}
116
- examples: [{}]
117
- greeting.get:
118
- cli: [greeting, get]
119
- summary: Read the greeting
120
- effect: read
121
- flow: {type: invoke, method: greeting.get}
122
- paramsSchema: {type: object, additionalProperties: false}
123
- examples: [{}]
124
- greeting.set:
125
- cli: [greeting, set]
126
- summary: Set the greeting
127
- effect: write
128
- flow: {type: invoke, method: greeting.set}
129
- paramsSchema:
130
- type: object
131
- required: [text]
132
- additionalProperties: false
133
- properties:
134
- text: {type: string, minLength: 1, maxLength: 200, description: Greeting text, examples: [Hello]}
135
- examples: [{text: Hello}]
109
+ registry: operations.json
@@ -0,0 +1,112 @@
1
+ {
2
+ "format": "synopkgland.api",
3
+ "version": 2,
4
+ "operations": {
5
+ "example.read": {
6
+ "summary": "Read a resource",
7
+ "ai": "read",
8
+ "cli": [
9
+ "example",
10
+ "read"
11
+ ],
12
+ "effect": "read",
13
+ "permission": "administrators",
14
+ "paramsSchema": {
15
+ "type": "object",
16
+ "additionalProperties": false
17
+ },
18
+ "resultSchema": {
19
+ "type": "object"
20
+ },
21
+ "flow": {
22
+ "type": "invoke",
23
+ "method": "example.read"
24
+ }
25
+ },
26
+ "example.preview": {
27
+ "summary": "Preview a resource change",
28
+ "ai": "preview",
29
+ "cli": [
30
+ "example",
31
+ "preview"
32
+ ],
33
+ "effect": "read",
34
+ "permission": "administrators",
35
+ "paramsSchema": {
36
+ "type": "object",
37
+ "additionalProperties": false
38
+ },
39
+ "resultSchema": {
40
+ "type": "object"
41
+ },
42
+ "flow": {
43
+ "type": "invoke",
44
+ "method": "example.preview"
45
+ }
46
+ },
47
+ "example.draft": {
48
+ "summary": "Draft a resource change for human confirmation",
49
+ "ai": "draft",
50
+ "cli": [
51
+ "example",
52
+ "draft"
53
+ ],
54
+ "effect": "write",
55
+ "permission": "administrators",
56
+ "paramsSchema": {
57
+ "type": "object",
58
+ "additionalProperties": false
59
+ },
60
+ "resultSchema": {
61
+ "type": "object"
62
+ },
63
+ "flow": {
64
+ "type": "invoke",
65
+ "method": "example.draft"
66
+ },
67
+ "preview": "example.preview"
68
+ },
69
+ "example.human": {
70
+ "summary": "Delete a resource after human review",
71
+ "ai": "human-only",
72
+ "cli": [
73
+ "example",
74
+ "human"
75
+ ],
76
+ "effect": "destructive",
77
+ "permission": "administrators",
78
+ "paramsSchema": {
79
+ "type": "object",
80
+ "additionalProperties": false
81
+ },
82
+ "resultSchema": {
83
+ "type": "object"
84
+ },
85
+ "flow": {
86
+ "type": "invoke",
87
+ "method": "example.human"
88
+ }
89
+ },
90
+ "example.hidden": {
91
+ "summary": "Read internal state",
92
+ "ai": "hidden",
93
+ "cli": [
94
+ "example",
95
+ "hidden"
96
+ ],
97
+ "effect": "read",
98
+ "permission": "administrators",
99
+ "paramsSchema": {
100
+ "type": "object",
101
+ "additionalProperties": false
102
+ },
103
+ "resultSchema": {
104
+ "type": "object"
105
+ },
106
+ "flow": {
107
+ "type": "invoke",
108
+ "method": "example.hidden"
109
+ }
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "format": "synopkgland.api",
3
+ "version": 2,
4
+ "operations": {
5
+ "app.status": {
6
+ "summary": "Read App status",
7
+ "ai": "read",
8
+ "cli": [
9
+ "app",
10
+ "status"
11
+ ],
12
+ "effect": "read",
13
+ "permission": "administrators",
14
+ "paramsSchema": {
15
+ "type": "object",
16
+ "additionalProperties": false
17
+ },
18
+ "resultSchema": {
19
+ "type": "object"
20
+ },
21
+ "flow": {
22
+ "type": "invoke",
23
+ "method": "app.status"
24
+ }
25
+ }
26
+ }
27
+ }
@@ -36,23 +36,13 @@
36
36
  function mount(root, ctx) {
37
37
  var t = ctx.t;
38
38
  var status = el("dl", { class: "dsmapp-facts" });
39
- var greeting = el("p", { class: "dsmapp-greeting", text: t("app.loading") });
40
- var setBy = el("p", { class: "dsmapp-note", hidden: "" });
41
- var input = el("input", { type: "text", maxlength: "200", "aria-label": t("greeting.label"), placeholder: t("greeting.label") });
42
- var save = el("button", { type: "submit", text: t("greeting.save") });
43
39
  var refreshButton = el("button", { type: "button", class: "dsmapp-secondary", text: t("app.refresh") });
44
40
  var message = el("p", { class: "dsmapp-message", role: "status" });
45
- var form = el("form", { class: "dsmapp-form", hidden: "" }, [input, save]);
46
- var adminOnly = el("p", { class: "dsmapp-note", hidden: "", text: t("greeting.adminOnly") });
47
41
  var portal = ctx.portal ? el("section", { class: "dsmapp-portal" }) : null;
48
42
 
49
43
  root.classList.add("dsmapp");
50
44
  root.replaceChildren(
51
45
  el("div", { class: "dsmapp-header" }, [el("h1", { text: t("app.title") }), refreshButton]),
52
- greeting,
53
- setBy,
54
- form,
55
- adminOnly,
56
46
  message,
57
47
  el("h2", { text: t("status.heading") }),
58
48
  status
@@ -80,24 +70,9 @@
80
70
  status.appendChild(el("dt", { text: row[0] }));
81
71
  status.appendChild(el("dd", { text: row[1] }));
82
72
  });
83
- form.hidden = !data.admin;
84
- adminOnly.hidden = data.admin;
85
- }).catch(showError);
86
- ctx.call("greeting.get").then(function (data) {
87
- greeting.textContent = data.text;
88
- setBy.textContent = data.updated_by ? t("greeting.setBy", { user: data.updated_by }) : "";
89
- setBy.hidden = !data.updated_by;
90
73
  }).catch(showError);
91
74
  }
92
75
 
93
- form.addEventListener("submit", function (event) {
94
- event.preventDefault();
95
- save.disabled = true;
96
- ctx.call("greeting.set", { text: input.value })
97
- .then(function () { input.value = ""; refresh(); })
98
- .catch(showError)
99
- .then(function () { save.disabled = false; });
100
- });
101
76
  refreshButton.addEventListener("click", refresh);
102
77
 
103
78
  ctx.reopen(refresh);