@voxgig/sdkgen 4.2.4 → 4.2.5

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 (69) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/AgentGuideTop.js +15 -7
  3. package/dist/cmp/AgentGuideTop.js.map +1 -1
  4. package/dist/cmp/ReadmeStation.js +88 -12
  5. package/dist/cmp/ReadmeStation.js.map +1 -1
  6. package/dist/helpers/station.d.ts +3 -0
  7. package/dist/helpers/station.js +67 -0
  8. package/dist/helpers/station.js.map +1 -0
  9. package/dist/sdkgen.d.ts +2 -1
  10. package/dist/sdkgen.js +41 -15
  11. package/dist/sdkgen.js.map +1 -1
  12. package/dist/tsconfig.tsbuildinfo +1 -1
  13. package/dist/types.d.ts +2 -0
  14. package/dist/utility.js +15 -1
  15. package/dist/utility.js.map +1 -1
  16. package/model/sdkgen.aon +20 -0
  17. package/package.json +1 -1
  18. package/project/.sdk/model/feature/audit.aon +1 -0
  19. package/project/.sdk/model/feature/cache.aon +1 -0
  20. package/project/.sdk/model/feature/clienttrack.aon +1 -0
  21. package/project/.sdk/model/feature/debug.aon +1 -0
  22. package/project/.sdk/model/feature/idempotency.aon +1 -0
  23. package/project/.sdk/model/feature/log.aon +1 -0
  24. package/project/.sdk/model/feature/metrics.aon +1 -0
  25. package/project/.sdk/model/feature/netsim.aon +1 -0
  26. package/project/.sdk/model/feature/paging.aon +1 -0
  27. package/project/.sdk/model/feature/proxy.aon +1 -0
  28. package/project/.sdk/model/feature/ratelimit.aon +1 -0
  29. package/project/.sdk/model/feature/rbac.aon +1 -0
  30. package/project/.sdk/model/feature/retry.aon +1 -0
  31. package/project/.sdk/model/feature/streaming.aon +1 -0
  32. package/project/.sdk/model/feature/telemetry.aon +1 -0
  33. package/project/.sdk/model/feature/test.aon +1 -0
  34. package/project/.sdk/model/feature/timeout.aon +1 -0
  35. package/project/.sdk/src/cmp/c/Config_c.ts +13 -10
  36. package/project/.sdk/src/cmp/clojure/Config_clojure.ts +4 -1
  37. package/project/.sdk/src/cmp/dart/Config_dart.ts +4 -1
  38. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +4 -1
  39. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +21 -2
  40. package/project/.sdk/src/cmp/go/Config_go.ts +4 -1
  41. package/project/.sdk/src/cmp/java/Config_java.ts +11 -8
  42. package/project/.sdk/src/cmp/js/Config_js.ts +4 -1
  43. package/project/.sdk/src/cmp/js/Main_js.ts +20 -1
  44. package/project/.sdk/src/cmp/js/fragment/MainStation.fragment.js +36 -0
  45. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +11 -7
  46. package/project/.sdk/src/cmp/lua/Config_lua.ts +4 -1
  47. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +3 -1
  48. package/project/.sdk/src/cmp/php/Config_php.ts +4 -1
  49. package/project/.sdk/src/cmp/py/Config_py.ts +4 -1
  50. package/project/.sdk/src/cmp/rb/Config_rb.ts +4 -1
  51. package/project/.sdk/src/cmp/swift/EntityTypes_swift.ts +2 -2
  52. package/project/.sdk/src/cmp/swift/Entity_swift.ts +3 -1
  53. package/project/.sdk/src/cmp/swift/Main_swift.ts +37 -2
  54. package/project/.sdk/src/cmp/swift/Package_swift.ts +9 -6
  55. package/project/.sdk/src/cmp/swift/ReadmeExamplesTest_swift.ts +1 -1
  56. package/project/.sdk/src/cmp/swift/ReadmeExplanation_swift.ts +5 -3
  57. package/project/.sdk/src/cmp/swift/Test_swift.ts +3 -1
  58. package/project/.sdk/src/cmp/swift/utility_swift.ts +16 -0
  59. package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
  60. package/project/.sdk/src/cmp/ts/Main_ts.ts +20 -1
  61. package/project/.sdk/src/cmp/ts/fragment/MainStation.fragment.ts +36 -0
  62. package/project/.sdk/src/cmp/zig/Config_zig.ts +4 -2
  63. package/project/sdkgen-package.json +1 -1
  64. package/src/cmp/AgentGuideTop.ts +15 -7
  65. package/src/cmp/ReadmeStation.ts +93 -13
  66. package/src/helpers/station.ts +80 -0
  67. package/src/sdkgen.ts +52 -15
  68. package/src/types.ts +6 -0
  69. package/src/utility.ts +15 -1
@@ -1,6 +1,8 @@
1
1
 
2
2
  import * as Path from 'node:path'
3
3
 
4
+ import { swiftTargetDir, swiftTestDir } from './utility_swift'
5
+
4
6
  import {
5
7
  cmp, each,
6
8
  File, Content, Copy, Folder, Fragment,
@@ -40,18 +42,51 @@ const Main = cmp(async function Main(props: any) {
40
42
 
41
43
  // Copy tm/swift files with replacements. `src/` holds only the per-feature
42
44
  // extension folders (not shipped into the SDK output).
45
+ // THE COPIED TREE HAS TO CARRY THE API NAME TOO.
46
+ //
47
+ // Copy substitutes file CONTENTS, never path components, so a blanket copy
48
+ // of tm/swift landed the runtime in a directory literally called
49
+ // ProjectNameSDK. Package.swift papered over it with an explicit `path:`,
50
+ // so it compiled and every swift suite passed — while every published SDK
51
+ // shipped `Sources/ProjectNameSDK/`, and SwiftPM's own convention
52
+ // (Sources/<target>) was broken in all of them.
53
+ //
54
+ // Copy's `to` prop names the destination, so the two placeholder subtrees
55
+ // are copied explicitly and the rest of tm/swift blanket-copied as before.
43
56
  Copy({
44
57
  from: 'tm/' + target.name,
45
- exclude: [/src\//],
58
+ exclude: [/src\//, /Sources\//, /Tests\//],
46
59
  replace: {
47
60
  ...props.ctx$.stdrep,
48
61
  ProjectName: model.const.Name,
49
62
  }
50
63
  })
51
64
 
65
+ Folder({ name: 'Sources' }, () => {
66
+ Copy({
67
+ from: 'tm/' + target.name + '/Sources/ProjectNameSDK',
68
+ to: swiftTargetDir(model),
69
+ replace: {
70
+ ...props.ctx$.stdrep,
71
+ ProjectName: model.const.Name,
72
+ }
73
+ })
74
+ })
75
+
76
+ Folder({ name: 'Tests' }, () => {
77
+ Copy({
78
+ from: 'tm/' + target.name + '/Tests/ProjectNameSDKTests',
79
+ to: swiftTestDir(model),
80
+ replace: {
81
+ ...props.ctx$.stdrep,
82
+ ProjectName: model.const.Name,
83
+ }
84
+ })
85
+ })
86
+
52
87
  // Generated sources join the copied runtime under Sources/ProjectNameSDK.
53
88
  Folder({ name: 'Sources' }, () => {
54
- Folder({ name: 'ProjectNameSDK' }, () => {
89
+ Folder({ name: swiftTargetDir(model) }, () => {
55
90
  Folder({ name: 'core' }, () => {
56
91
 
57
92
  // Main SDK client class, with entity accessors injected at the SLOT.
@@ -1,4 +1,6 @@
1
1
 
2
+ import { swiftTargetDir, swiftTestDir } from './utility_swift'
3
+
2
4
  import {
3
5
  Content,
4
6
  File,
@@ -14,10 +16,11 @@ import type {
14
16
 
15
17
  // Emits Package.swift (the SwiftPM manifest; the Swift twin of Package_go's
16
18
  // go.mod / Package_csharp's csproj). The library target compiles everything
17
- // under Sources/ProjectNameSDK (the copied runtime + generated sources); the
18
- // test target compiles Tests/ProjectNameSDKTests. Directory names are the
19
- // verbatim copied paths (Copy does not rewrite path components); the target
20
- // NAMES carry the API name.
19
+ // under Sources/<Name>Sdk (the copied runtime + generated sources); the test
20
+ // target compiles Tests/<Name>SdkTests. Both directories carry the API name:
21
+ // Copy does not rewrite path components, so Main_swift copies the two
22
+ // placeholder subtrees explicitly via Copy's `to` prop rather than letting a
23
+ // blanket copy ship them as ProjectNameSDK.
21
24
  //
22
25
  // Dependencies: the runtime itself is dependency-free (Foundation + the
23
26
  // vendored struct), but declared target/feature deps flow into the manifest
@@ -91,11 +94,11 @@ let package = Package(
91
94
  targets: [
92
95
  .target(
93
96
  name: "${Name}Sdk",${targetdeps}
94
- path: "Sources/ProjectNameSDK"),
97
+ path: "Sources/${swiftTargetDir(model)}"),
95
98
  .testTarget(
96
99
  name: "${Name}SdkTests",
97
100
  dependencies: ["${Name}Sdk"],
98
- path: "Tests/ProjectNameSDKTests"),
101
+ path: "Tests/${swiftTestDir(model)}"),
99
102
  ]
100
103
  )
101
104
  `)
@@ -64,7 +64,7 @@ final class ReadmeExamplesTest: XCTestCase {
64
64
  // The three documents to scan, resolved relative to this test file.
65
65
  private func docs() -> [Doc] {
66
66
  let here = URL(fileURLWithPath: #filePath)
67
- .deletingLastPathComponent() // ProjectNameSDKTests
67
+ .deletingLastPathComponent() // the test target dir
68
68
  .deletingLastPathComponent() // Tests
69
69
  .deletingLastPathComponent() // swift (module dir)
70
70
  let project = here.deletingLastPathComponent() // project (holds root README)
@@ -1,4 +1,6 @@
1
1
 
2
+ import { swiftTargetDir, swiftTestDir } from './utility_swift'
3
+
2
4
  import { cmp, Content } from '@voxgig/sdkgen'
3
5
 
4
6
 
@@ -24,16 +26,16 @@ generated for editor documentation.
24
26
  \`\`\`
25
27
  swift/
26
28
  ├── Package.swift -- SwiftPM manifest (zero runtime deps)
27
- ├── Sources/ProjectNameSDK/
29
+ ├── Sources/${swiftTargetDir(model)}/
28
30
  │ ├── core/ -- Main client, config, entity base, error type
29
31
  │ ├── entity/ -- Generated entity clients
30
32
  │ ├── feature/ -- Built-in features (Base, Test, Log, ...)
31
33
  │ ├── utility/ -- Utility functions
32
34
  │ └── Struct/ -- Vendored Voxgig Struct port
33
- └── Tests/ProjectNameSDKTests/ -- Test suites (XCTest)
35
+ └── Tests/${swiftTestDir(model)}/ -- Test suites (XCTest)
34
36
  \`\`\`
35
37
 
36
- The main client class (\`${SDK}\`, under \`Sources/ProjectNameSDK/core\`)
38
+ The main client class (\`${SDK}\`, under \`Sources/${swiftTargetDir(model)}/core\`)
37
39
  exposes the entity accessors. Reference entity or utility types directly only
38
40
  when needed. The SDK is dependency-free: JSON parsing is the vendored
39
41
  \`Struct/JSON.swift\`, HTTP transport is Foundation's \`URLSession\`, and the
@@ -3,6 +3,8 @@ import type {
3
3
  ModelEntity
4
4
  } from '@voxgig/apidef'
5
5
 
6
+ import { swiftTargetDir, swiftTestDir } from './utility_swift'
7
+
6
8
  import { cmp, each, Folder, entityCollection } from '@voxgig/sdkgen'
7
9
 
8
10
 
@@ -20,7 +22,7 @@ const Test = cmp(function Test(props: any) {
20
22
  // templates in tm/swift/Tests. Only the API-specific entity/direct tests
21
23
  // are generated here.
22
24
  Folder({ name: 'Tests' }, () => {
23
- Folder({ name: 'ProjectNameSDKTests' }, () => {
25
+ Folder({ name: swiftTestDir(model) }, () => {
24
26
  const entity = each(entityCollection(model))
25
27
  .filter((e: any) => false !== e.active)
26
28
 
@@ -132,10 +132,26 @@ function clean(o: any, dropDefaults?: boolean): any {
132
132
  }
133
133
 
134
134
 
135
+ // SwiftPM's convention is Sources/<target>/ and Tests/<target>Tests/, and the
136
+ // target is named <Name>Sdk in Package_swift. These name the two directories
137
+ // so the copied runtime lands under the API's own name rather than under the
138
+ // template's placeholder — see the note in Main_swift.
139
+ function swiftTargetDir(model: any): string {
140
+ return model.const.Name + 'Sdk'
141
+ }
142
+
143
+
144
+ function swiftTestDir(model: any): string {
145
+ return swiftTargetDir(model) + 'Tests'
146
+ }
147
+
148
+
135
149
  export {
136
150
  clean,
137
151
  projectPath,
138
152
  swiftPascalName,
153
+ swiftTargetDir,
154
+ swiftTestDir,
139
155
  swiftVarName,
140
156
  swiftString,
141
157
  }
@@ -142,8 +142,11 @@ const Config = cmp(async function Config(props: any) {
142
142
  Line(` ${f.name}: ${nom(f, 'Name')}Feature,`)
143
143
  }),
144
144
 
145
+ // Rendered from configDefinition's def, not from f.config, so the
146
+ // literal carries the feature's `transport` role (station design
147
+ // §8.4) beside its options and cannot drift from the data rep.
145
148
  '// #FeatureConfigs': () => each(feature, (f: any) => {
146
- Line(` ${f.name}: ${formatJson(f.config, { margin: 4 })},`)
149
+ Line(` ${f.name}: ${formatJson(configDef.feature[f.name], { margin: 4 })},`)
147
150
  }),
148
151
 
149
152
 
@@ -4,7 +4,7 @@ import * as Path from 'node:path'
4
4
  import {
5
5
  cmp, each, names, cmap,
6
6
  List, File, Content, Copy, Folder, Fragment, Line, FeatureHook,
7
- entityClassName, entityCollection, srcFeatureExcludes,
7
+ entityClassName, entityCollection, srcFeatureExcludes, stationLibrary,
8
8
  } from '@voxgig/sdkgen'
9
9
 
10
10
 
@@ -106,6 +106,25 @@ if (fres instanceof Promise) { await fres }
106
106
  MainEntity(entprops)
107
107
  })
108
108
  })
109
+
110
+ // Station self-registration (station design §6.2 path 1;
111
+ // station-declarative-config §11 item 2): emitted ONLY when the model
112
+ // carries an ACTIVE station feature (installed via
113
+ // `package add @voxgig/sdkgen-station`). The library package name
114
+ // comes from the feature model's own `deps.ts` block — the same entry
115
+ // collectDeps flows into package.json — so the manifest dependency
116
+ // and the emitted require cannot disagree.
117
+ const stationPkg = stationLibrary(model, target.name)
118
+ if (null != stationPkg) {
119
+ Fragment({
120
+ from: Path.normalize(
121
+ __dirname + '/../../../src/cmp/ts/fragment/MainStation.fragment.ts'),
122
+ replace: {
123
+ ...props.ctx$.stdrep,
124
+ "'STATIONPKG'": JSON.stringify(stationPkg),
125
+ }
126
+ })
127
+ }
109
128
  })
110
129
 
111
130
  Config({ target })
@@ -0,0 +1,36 @@
1
+
2
+
3
+ // Station self-registration (voxgig/station design §6.2, path 1): linking
4
+ // this package is the whole bootstrap. When the station library is
5
+ // installed beside this SDK, the {construct, config} factory pair is
6
+ // registered under the api slug (config.main.slug, the descriptor slug) at
7
+ // module init, so `station.sdk('<name>')` needs no imports in application
8
+ // code. Registration is SOFT — the station library is a peer dependency of
9
+ // the station feature, and a project without it skips registration and
10
+ // gets exactly the SDK it always had. Only a genuine module-not-found is
11
+ // swallowed: a station that RESOLVES but throws while loading must
12
+ // propagate (a silently empty factory table has no visible cause), and so
13
+ // must station_factory_conflict — two different builds of one SDK in one
14
+ // process is not a thing to pick between quietly.
15
+ let stationInstalled = true
16
+ try { require.resolve('STATIONPKG') }
17
+ catch (err: any) {
18
+ // MODULE_NOT_FOUND is the absent case. Anything else - an invalid
19
+ // package config, a root entry the package does not export - is a
20
+ // real failure to load a station that IS installed, and swallowing
21
+ // it would leave an empty factory table with no visible cause.
22
+ if ('MODULE_NOT_FOUND' !== err?.code) { throw err }
23
+ stationInstalled = false
24
+ }
25
+
26
+ if (stationInstalled) {
27
+ const { provide } = require('STATIONPKG')
28
+ // A station library predating the factory table has no provide() —
29
+ // there is nothing to register with, which is not this SDK's error.
30
+ if ('function' === typeof provide) {
31
+ provide(config.main.slug, {
32
+ construct: (options?: any) => new ProjectNameSDK(options),
33
+ config,
34
+ })
35
+ }
36
+ }
@@ -38,8 +38,10 @@ const Config = cmp(async function Config(props: any) {
38
38
  // representations render from the same `def`, so they cannot describe
39
39
  // different configs - and this target picks up `options.server` (the
40
40
  // OpenAPI server-variable defaults), which the hand-rolled build here
41
- // omitted entirely.
42
- const { def: config, json: configJson } = configDefinition(model)
41
+ // omitted entirely. Passing target.name opts this target into the main
42
+ // slug/version/target identity fields (read by station's descriptor -
43
+ // see configDefinition).
44
+ const { def: config, json: configJson } = configDefinition(model, target.name)
43
45
  const asData = isConfigData(configJson, configReprSetting(model))
44
46
 
45
47
  File({ name: 'config.' + target.ext }, () => {
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.2.4",
6
+ "version": "4.2.5",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",
@@ -83,19 +83,27 @@ package (other languages). Each target's guide documents its features.
83
83
  `)
84
84
  }
85
85
 
86
- // Station paragraph (station design §9.4), only when the model
87
- // carries the feature: an agent working on this repo should know
88
- // the runtime story without leaving AGENTS.md.
86
+ // Station paragraph (station design §9.4; declarative design §11
87
+ // item 4), only when the model carries the feature: an agent working
88
+ // on this repo should know the runtime story without leaving
89
+ // AGENTS.md — and, first, that the application's integrations are
90
+ // DECLARED in station.json, so that file is where to look.
89
91
  if (features.some((f: any) => 'station' === f.name)) {
90
92
  Content(`**Station**: this SDK is a
91
93
  [voxgig/station](https://github.com/voxgig/station) plugin (the
92
- \`station\` feature, off by default). Bound to an open \`Station\`,
93
- the credential is resolved by sekreto under the plugin's secret name
94
+ \`station\` feature, off by default). An application's outbound
95
+ integrations are **declared in \`station.json\`** at its repo root
96
+ to learn what the application talks to, read that file: every SDK
97
+ instance (\`sdk\`), per-api default (\`api\`), feature setting, and
98
+ egress policy is declared there, and never a credential value.
99
+ \`station.sdk('<name>')\` builds a declared instance on first ask;
100
+ \`station.instances()\` lists them. Bound to an open \`Station\`,
101
+ the credential is resolved by sekreto under the instance's secret name
94
102
  and injected at the transport seam — \`options()\` and
95
103
  \`prepare()\` output hold only a placeholder, so both are safe to
96
104
  inspect and log. \`station.tap(...)\`/\`station.events()\` show live
97
- traffic; \`station.plugins()\` lists descriptors. See the "Use with
98
- Station" README section and \`src/feature/station/\` (or the
105
+ traffic; \`station.plugins()\` lists live descriptors. See the "Use
106
+ with Station" README section and \`src/feature/station/\` (or the
99
107
  target's feature container) for the generated adapter.
100
108
 
101
109
  `)
@@ -6,20 +6,39 @@ import {
6
6
  getModelPath
7
7
  } from '../types'
8
8
 
9
- import { envName } from '../helpers/packageMeta'
9
+ import { envName, packageName } from '../helpers/packageMeta'
10
10
 
11
11
 
12
12
  // The "Use with Station" README section (station design §9.4): rendered
13
13
  // ONLY when the project's model carries the station feature (installed
14
14
  // via `package add @voxgig/sdkgen-station`) — a project without it sees
15
- // nothing. Documents the binding forms and the secret name; store
16
- // configuration is sekreto's documentation, deliberately not restated
17
- // here (one canonical source).
15
+ // nothing. Leads with the DECLARATIVE flow a `station.json` block and
16
+ // `station.sdk()` (declarative design §11 item 3) — keeps the imperative
17
+ // `connect()` form as the retrofit path, and documents the
18
+ // instance-derived secret/env-var name (declarative design §3.4, §5.1:
19
+ // the same envtoken grammar as sdkgen's envName, applied to the INSTANCE
20
+ // name, so the untagged instance keeps the env var this README already
21
+ // documents to the byte). Store configuration is sekreto's
22
+ // documentation, deliberately not restated here (one canonical source);
23
+ // the error codes live in sdkgen's one catalog page, linked rather than
24
+ // restated for the same reason.
18
25
 
19
26
  // Targets where station.connect(SDK) is the idiomatic binding; everything
20
27
  // else uses inverted binding through the SDK's own constructor.
21
28
  const CONNECT_TARGETS = ['ts', 'js', 'py', 'rb', 'php', 'lua', 'perl']
22
29
 
30
+ // Targets whose module system has an init hook that actually runs
31
+ // (station design §6.2 path 1): there, linking the generated package
32
+ // fills the process-global factory table and `station.sdk()` needs no
33
+ // application code. Everywhere else the README must say
34
+ // `Station.provide` (path 2) plainly rather than imply an import is
35
+ // enough — a Java import is a compile-time alias that runs nothing.
36
+ const SELF_REGISTER_TARGETS =
37
+ ['ts', 'js', 'go', 'py', 'rb', 'php', 'lua', 'perl', 'elixir', 'clojure']
38
+
39
+ const ERROR_CATALOG_URL =
40
+ 'https://github.com/voxgig/sdkgen/blob/main/docs/reference/station-errors.md'
41
+
23
42
  const ReadmeStation = cmp(function ReadmeStation(props: any) {
24
43
  const { target } = props
25
44
  const { model } = props.ctx$
@@ -30,9 +49,15 @@ const ReadmeStation = cmp(function ReadmeStation(props: any) {
30
49
  .find((f: any) => 'station' === f?.name)
31
50
  if (null == station) { return }
32
51
 
52
+ // The descriptor slug is the model's hyphenated name (carried as
53
+ // main.slug in the embedded config); an untagged instance ref IS the
54
+ // slug, so this is also the default instance name.
55
+ const slug = model.name
33
56
  const env = envName(model)
34
- const secretname = env.toLowerCase() + '.apikey'
57
+ const secretbase = env.toLowerCase()
58
+ const secretname = secretbase + '.apikey'
35
59
  const connect = CONNECT_TARGETS.includes(target.name)
60
+ const selfreg = SELF_REGISTER_TARGETS.includes(target.name)
36
61
 
37
62
  Content(`
38
63
  ## Use with Station
@@ -42,24 +67,79 @@ plugin: bind it to a local \`Station\` and outbound configuration,
42
67
  credentials, and observability move to one place. The feature is
43
68
  present but **off by default** — nothing changes until you bind.
44
69
 
70
+ ### Declarative: \`station.json\` + \`station.sdk()\`
71
+
72
+ Declare an instance in \`station.json\` at the repo root (committable —
73
+ names and stores, never values):
74
+
75
+ \`\`\`json
76
+ { "station": 1,
77
+ "profiles": { "default": {
78
+ "sdk": { "${slug}": {${selfreg ?
79
+ `\n "package": "${packageName(model, target.name)}"` : ''} } } } } }
80
+ \`\`\`
81
+
82
+ Then get the client where you need it:
83
+
84
+ 1. \`station = Station.open()\` — reads and validates \`station.json\`;
85
+ constructs nothing.
86
+ 2. \`client = station.sdk('${slug}')\` — built on first ask and cached,
87
+ so the same name returns the same client.
88
+
89
+ ${selfreg
90
+ ? `Loading this package is the whole bootstrap: it registers its own
91
+ factory (constructor plus embedded config) with the station library at
92
+ module init, so \`station.sdk()\` needs no SDK import in application
93
+ code. That is what the \`package\` key above is for — it names the module
94
+ for station to load, since nothing else in this example would execute
95
+ it. An application that imports this SDK for its types anyway can drop
96
+ the key: that import is itself the bootstrap.`
97
+ : `In this language an import runs no code, so register the factory
98
+ once at startup — \`Station.provide('${slug}', ...)\`, one line — and
99
+ every other line of configuration stays in \`station.json\`.`}
100
+
101
+ A second instance of the same API is one more key — \`"${slug}$test"\`
102
+ beside \`"${slug}"\` — and \`station.sdk('${slug}$test')\` returns it.
103
+ SDK features are configured in the same file too: fleet-wide, per api,
104
+ or per instance. \`station.check()\` resolves and constructs every
105
+ active instance without sending a request — run it in CI.
106
+
107
+ ### The secret name derives from the instance
108
+
109
+ The credential comes from [sekreto](https://github.com/voxgig/sekreto)
110
+ under a name derived from the **instance** name — the instance token
111
+ lowercased, plus \`.apikey\`. The untagged instance \`${slug}\` derives
112
+ \`${secretname}\` — by default the \`${env}_APIKEY\` environment
113
+ variable this README already documents, unchanged. A tagged instance
114
+ derives its own: \`${slug}$test\` → \`${secretbase}_test.apikey\` →
115
+ \`${env}_TEST_APIKEY\` — each instance is a separate credentialed use
116
+ of the API, and its env var is derivable from the name you chose. To
117
+ pin a name instead, set \`secret\` on the instance block, or at the
118
+ api level for several instances sharing one key. Point a profile at a
119
+ vault later and application code does not change; sekreto's own
120
+ documentation covers the stores. The key stays out of \`options()\` and
121
+ \`prepare()\` output; \`station.tap(...)\` shows live traffic.
122
+
123
+ ### Imperative: ${connect ? '`connect()`' : 'inverted binding'} — the retrofit path
124
+
45
125
  ${connect
46
126
  ? `Bind by passing the SDK class to the station:
47
127
 
48
128
  1. \`station = Station.open()\` — profile, env, and proxy all defaulted.
49
129
  2. \`client = station.connect(${model.const.Name}SDK)\` — replaces direct
50
- construction.`
130
+ construction; \`connect(${model.const.Name}SDK, { as: 'test' })\` binds
131
+ a second, tagged instance.`
51
132
  : `Bind through the constructor this SDK already has (inverted
52
133
  binding): open a station, then construct with station-built options —
53
134
  \`station.options()\` merges the handle, the activation entry, and the
54
135
  correct feature order into the plain options the constructor accepts.`}
55
136
 
56
- The credential comes from [sekreto](https://github.com/voxgig/sekreto)
57
- under the name \`${secretname}\` by default the \`${env}_APIKEY\`
58
- environment variable this README already documents, unchanged. Point a
59
- profile in \`station.json\` at a vault later and application code does
60
- not change; sekreto's own documentation covers the stores. The key
61
- stays out of \`options()\` and \`prepare()\` output; \`station.tap(...)\`
62
- shows live traffic.
137
+ With no \`station.json\` at all this runs solo with everything
138
+ defaulted the two-line form is how an existing application starts.
139
+
140
+ Station failures surface through this SDK's own error path
141
+ (\`err.code\`, \`station_*\`); the codes are catalogued in
142
+ [station error codes](${ERROR_CATALOG_URL}).
63
143
  `)
64
144
  })
65
145
 
@@ -0,0 +1,80 @@
1
+ // The station SELF-REGISTRATION seam (station design station.md §9 /
2
+ // station-declarative-config.md §6.2 path 1, §11 item 2): when the station
3
+ // feature is installed and active, the generated MAIN module registers the
4
+ // SDK's `{construct, config}` factory pair with the station library at
5
+ // module init, so `station.sdk('<name>')` needs no imports in application
6
+ // code. The registration key is the descriptor slug — `config.main.slug`,
7
+ // the same field station's `normalizeDescriptor` reads — so the generated
8
+ // code passes the embedded config's own value rather than re-deriving it.
9
+ //
10
+ // This helper answers the ONE question every Main_<lang> asks before
11
+ // emitting that registration: is the station feature ACTIVE in this model,
12
+ // and what is the station library package for this target? Both come from
13
+ // the model — the active-filtered feature map (exactly the view the Main
14
+ // components already emit from) and the station feature's own
15
+ // `deps.<target>` block, which is the same entry `collectDeps` flows into
16
+ // the generated manifest. ONE RULE, ONE PLACE: reading the package name
17
+ // here, never hardcoding it per language, means the manifest dependency
18
+ // and the emitted require can not disagree.
19
+ //
20
+ // Returns undefined when the station feature is absent, inactive, or
21
+ // declares no active station library dep for this target (the vendored
22
+ // targets carry the library inside their tm overlay instead of as a dep —
23
+ // station.md §9.2): no dep, no package to require, no registration to emit.
24
+
25
+ import { each } from 'jostraca'
26
+
27
+ import {
28
+ KIT,
29
+ getModelPath,
30
+ } from '@voxgig/apidef'
31
+
32
+ import { SdkGenError } from '../utility'
33
+
34
+ import type { SdkModel, ModelFeature } from '../types'
35
+
36
+
37
+ function stationLibrary(model: SdkModel, targetName: string): string | undefined {
38
+ // Active-filtered on purpose: an inactive feature ships no source, no
39
+ // embedded config entry and no manifest dep, so it must emit no
40
+ // registration either.
41
+ const feature = getModelPath(model, `main.${KIT}.feature`,
42
+ { required: false }) || {}
43
+
44
+ const station: ModelFeature | undefined = feature.station
45
+ if (null == station) { return undefined }
46
+
47
+ const deps = station.deps?.[targetName]
48
+ if (null == deps) { return undefined }
49
+
50
+ // Feature deps count only when explicitly active — collectDeps semantics,
51
+ // so the require target is exactly the set the manifest carries.
52
+ const names = each(deps)
53
+ .filter((dep: any) => true === dep?.active)
54
+ .map((dep: any) => dep?.key$)
55
+ .filter((name: any) => null != name && '' !== name)
56
+
57
+ if (0 === names.length) { return undefined }
58
+
59
+ // EXACTLY ONE, or say so. Picking the first of several — each() sorts,
60
+ // so it would be the alphabetically first — means the generated main
61
+ // can `require` an unrelated helper package, find no `provide`, and
62
+ // leave the factory table silently empty. The station feature model
63
+ // declares one library per target; a second active dep is a model
64
+ // question only its author can answer.
65
+ if (1 < names.length) {
66
+ throw new SdkGenError(
67
+ 'station: feature `station` declares ' + names.length + ' active ' +
68
+ 'dependencies for target `' + targetName + '` (' +
69
+ names.map(String).sort().join(', ') + '), so the station library to ' +
70
+ 'register with is ambiguous. Declare exactly one active dep per ' +
71
+ 'target in the feature model, or mark which one is the library.')
72
+ }
73
+
74
+ return String(names[0])
75
+ }
76
+
77
+
78
+ export {
79
+ stationLibrary,
80
+ }