@pikku/core 0.12.63 → 0.12.66

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 (225) hide show
  1. package/CHANGELOG.md +368 -0
  2. package/dist/dev/hot-reload.js +13 -55
  3. package/dist/dev/module-runner.d.ts +14 -0
  4. package/dist/dev/module-runner.js +59 -0
  5. package/dist/dev/reload-meta.d.ts +13 -0
  6. package/dist/dev/reload-meta.js +22 -0
  7. package/dist/errors/errors.d.ts +12 -0
  8. package/dist/errors/errors.js +19 -0
  9. package/dist/function/function-runner.d.ts +5 -5
  10. package/dist/function/function-runner.js +39 -111
  11. package/dist/function/functions.types.d.ts +17 -7
  12. package/dist/index.d.ts +5 -1
  13. package/dist/index.js +3 -1
  14. package/dist/middleware/auth-apikey.d.ts +1 -0
  15. package/dist/middleware/auth-bearer.d.ts +1 -0
  16. package/dist/middleware/auth-cookie.d.ts +1 -0
  17. package/dist/middleware/cors.d.ts +1 -0
  18. package/dist/middleware/index.d.ts +1 -1
  19. package/dist/middleware/index.js +1 -1
  20. package/dist/middleware/remote-auth.d.ts +1 -0
  21. package/dist/middleware/telemetry.d.ts +2 -0
  22. package/dist/middleware/timeout.d.ts +1 -0
  23. package/dist/permissions.d.ts +42 -27
  24. package/dist/permissions.js +101 -203
  25. package/dist/pikku-state.js +0 -2
  26. package/dist/scopes.d.ts +17 -0
  27. package/dist/scopes.js +59 -0
  28. package/dist/services/ai-embedding-service.d.ts +31 -0
  29. package/dist/services/ai-embedding-service.js +1 -0
  30. package/dist/services/credential-wire-service.d.ts +3 -1
  31. package/dist/services/credential-wire-service.js +10 -4
  32. package/dist/services/index.d.ts +4 -0
  33. package/dist/services/index.js +2 -0
  34. package/dist/services/meta-service.d.ts +15 -2
  35. package/dist/services/meta-service.js +51 -1
  36. package/dist/services/queue-webhook-service.d.ts +55 -0
  37. package/dist/services/queue-webhook-service.js +136 -0
  38. package/dist/services/scope-service.d.ts +67 -0
  39. package/dist/services/scope-service.js +1 -0
  40. package/dist/services/typed-secret-service.d.ts +8 -0
  41. package/dist/services/typed-secret-service.js +39 -4
  42. package/dist/services/webhook-service.d.ts +140 -0
  43. package/dist/services/webhook-service.js +44 -0
  44. package/dist/services/workflow-service.d.ts +7 -0
  45. package/dist/testing/service-tests.js +37 -0
  46. package/dist/types/core.types.d.ts +34 -6
  47. package/dist/types/state.types.d.ts +20 -2
  48. package/dist/utils/hmac.d.ts +16 -0
  49. package/dist/utils/hmac.js +26 -0
  50. package/dist/utils/safe-fetch.d.ts +51 -0
  51. package/dist/utils/safe-fetch.js +192 -0
  52. package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
  53. package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
  54. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +105 -1
  55. package/dist/wirings/ai-agent/ai-agent-prepare.js +249 -11
  56. package/dist/wirings/ai-agent/ai-agent-runner.js +10 -1
  57. package/dist/wirings/ai-agent/ai-agent-stream.js +88 -16
  58. package/dist/wirings/ai-agent/ai-agent.types.d.ts +56 -2
  59. package/dist/wirings/ai-agent/index.d.ts +2 -1
  60. package/dist/wirings/ai-agent/index.js +2 -1
  61. package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
  62. package/dist/wirings/ai-agent/voice-input.js +5 -41
  63. package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
  64. package/dist/wirings/channel/channel-common.js +0 -1
  65. package/dist/wirings/channel/channel-handler.js +1 -4
  66. package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
  67. package/dist/wirings/channel/channel-middleware-runner.js +34 -19
  68. package/dist/wirings/channel/channel.types.d.ts +2 -6
  69. package/dist/wirings/cli/cli-runner.js +3 -2
  70. package/dist/wirings/cli/cli.types.d.ts +1 -2
  71. package/dist/wirings/cli/command-parser.d.ts +2 -0
  72. package/dist/wirings/cli/command-parser.js +59 -2
  73. package/dist/wirings/credential/credential.types.d.ts +14 -0
  74. package/dist/wirings/credential/validate-credential-definitions.js +1 -0
  75. package/dist/wirings/gateway/gateway-runner.js +100 -50
  76. package/dist/wirings/gateway/gateway.types.d.ts +8 -5
  77. package/dist/wirings/http/http-routes.js +0 -3
  78. package/dist/wirings/http/http-runner.d.ts +9 -29
  79. package/dist/wirings/http/http-runner.js +9 -34
  80. package/dist/wirings/http/http.types.d.ts +4 -12
  81. package/dist/wirings/mcp/mcp-runner.js +0 -2
  82. package/dist/wirings/mcp/mcp.types.d.ts +5 -11
  83. package/dist/wirings/oauth2/index.d.ts +0 -3
  84. package/dist/wirings/oauth2/index.js +1 -2
  85. package/dist/wirings/rpc/addon-runner.d.ts +28 -0
  86. package/dist/wirings/rpc/addon-runner.js +173 -0
  87. package/dist/wirings/rpc/index.d.ts +5 -1
  88. package/dist/wirings/rpc/index.js +3 -1
  89. package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
  90. package/dist/wirings/rpc/remote-addon-auth.js +43 -0
  91. package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
  92. package/dist/wirings/rpc/rpc-runner.js +109 -3
  93. package/dist/wirings/rpc/rpc-types.d.ts +15 -1
  94. package/dist/wirings/rpc/wire-addon.js +9 -0
  95. package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
  96. package/dist/wirings/rpc/wire-remote-addon.js +19 -0
  97. package/dist/wirings/scope/index.d.ts +3 -0
  98. package/dist/wirings/scope/index.js +2 -0
  99. package/dist/wirings/scope/scope.types.d.ts +39 -0
  100. package/dist/wirings/scope/scope.types.js +1 -0
  101. package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
  102. package/dist/wirings/scope/validate-scope-definitions.js +76 -0
  103. package/dist/wirings/scope/wire-scope.d.ts +33 -0
  104. package/dist/wirings/scope/wire-scope.js +32 -0
  105. package/dist/wirings/secret/secret.types.d.ts +14 -0
  106. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  107. package/dist/wirings/variable/validate-variable-definitions.js +2 -0
  108. package/dist/wirings/variable/variable.types.d.ts +14 -0
  109. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  110. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +89 -7
  111. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
  112. package/dist/wirings/workflow/graph/graph-runner.js +3 -0
  113. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
  114. package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
  115. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
  116. package/dist/wirings/workflow/index.d.ts +2 -2
  117. package/dist/wirings/workflow/index.js +1 -1
  118. package/dist/wirings/workflow/pikku-workflow-service.d.ts +59 -1
  119. package/dist/wirings/workflow/pikku-workflow-service.js +187 -0
  120. package/dist/wirings/workflow/workflow.types.d.ts +1 -3
  121. package/package.json +6 -1
  122. package/run-tests.sh +1 -0
  123. package/src/dev/hot-reload.ts +13 -68
  124. package/src/dev/module-runner.test.ts +169 -0
  125. package/src/dev/module-runner.ts +103 -0
  126. package/src/dev/reload-meta.test.ts +31 -2
  127. package/src/dev/reload-meta.ts +26 -0
  128. package/src/errors/errors.ts +24 -0
  129. package/src/function/function-runner.test.ts +276 -142
  130. package/src/function/function-runner.ts +52 -145
  131. package/src/function/functions.types.ts +28 -38
  132. package/src/index.ts +8 -6
  133. package/src/middleware/index.ts +1 -5
  134. package/src/permissions.test.ts +160 -373
  135. package/src/permissions.ts +137 -279
  136. package/src/pikku-state.ts +0 -2
  137. package/src/scopes.test.ts +167 -0
  138. package/src/scopes.ts +69 -0
  139. package/src/services/ai-embedding-service.ts +31 -0
  140. package/src/services/credential-wire-service.ts +10 -4
  141. package/src/services/index.ts +20 -0
  142. package/src/services/meta-service.ts +68 -3
  143. package/src/services/queue-webhook-service.test.ts +408 -0
  144. package/src/services/queue-webhook-service.ts +182 -0
  145. package/src/services/scope-service.ts +75 -0
  146. package/src/services/typed-secret-service.test.ts +35 -0
  147. package/src/services/typed-secret-service.ts +39 -4
  148. package/src/services/webhook-service.ts +180 -0
  149. package/src/services/workflow-service.ts +7 -0
  150. package/src/testing/service-tests.ts +49 -0
  151. package/src/types/core.types.ts +42 -14
  152. package/src/types/state.types.ts +23 -3
  153. package/src/utils/hmac.ts +27 -0
  154. package/src/utils/safe-fetch.test.ts +373 -0
  155. package/src/utils/safe-fetch.ts +213 -0
  156. package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
  157. package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
  158. package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
  159. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +483 -0
  160. package/src/wirings/ai-agent/ai-agent-prepare.ts +324 -13
  161. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
  162. package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
  163. package/src/wirings/ai-agent/ai-agent-runner.ts +28 -0
  164. package/src/wirings/ai-agent/ai-agent-stream.test.ts +301 -10
  165. package/src/wirings/ai-agent/ai-agent-stream.ts +122 -16
  166. package/src/wirings/ai-agent/ai-agent.types.ts +57 -0
  167. package/src/wirings/ai-agent/index.ts +5 -0
  168. package/src/wirings/ai-agent/voice-input.test.ts +90 -0
  169. package/src/wirings/ai-agent/voice-input.ts +9 -40
  170. package/src/wirings/channel/channel-common.ts +0 -1
  171. package/src/wirings/channel/channel-handler.ts +1 -9
  172. package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
  173. package/src/wirings/channel/channel-middleware-runner.ts +39 -27
  174. package/src/wirings/channel/channel.types.ts +0 -6
  175. package/src/wirings/cli/cli-runner.ts +4 -2
  176. package/src/wirings/cli/cli.types.ts +0 -2
  177. package/src/wirings/cli/command-parser.test.ts +130 -0
  178. package/src/wirings/cli/command-parser.ts +80 -2
  179. package/src/wirings/credential/credential.types.ts +14 -0
  180. package/src/wirings/credential/validate-credential-definitions.ts +1 -0
  181. package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
  182. package/src/wirings/gateway/gateway-runner.ts +114 -68
  183. package/src/wirings/gateway/gateway.types.ts +7 -9
  184. package/src/wirings/http/http-routes.ts +0 -3
  185. package/src/wirings/http/http-runner.test.ts +1 -11
  186. package/src/wirings/http/http-runner.ts +16 -39
  187. package/src/wirings/http/http.types.ts +7 -14
  188. package/src/wirings/mcp/mcp-runner.ts +0 -2
  189. package/src/wirings/mcp/mcp.types.ts +3 -11
  190. package/src/wirings/oauth2/index.ts +0 -3
  191. package/src/wirings/rpc/addon-runner.ts +233 -0
  192. package/src/wirings/rpc/index.ts +17 -1
  193. package/src/wirings/rpc/remote-addon-auth.ts +69 -0
  194. package/src/wirings/rpc/rpc-runner.test.ts +319 -16
  195. package/src/wirings/rpc/rpc-runner.ts +155 -3
  196. package/src/wirings/rpc/rpc-types.ts +16 -5
  197. package/src/wirings/rpc/wire-addon.test.ts +6 -3
  198. package/src/wirings/rpc/wire-addon.ts +9 -0
  199. package/src/wirings/rpc/wire-remote-addon.ts +57 -0
  200. package/src/wirings/scope/index.ts +14 -0
  201. package/src/wirings/scope/scope.test.ts +135 -0
  202. package/src/wirings/scope/scope.types.ts +44 -0
  203. package/src/wirings/scope/validate-scope-definitions.ts +110 -0
  204. package/src/wirings/scope/wire-scope.ts +34 -0
  205. package/src/wirings/secret/secret.types.ts +14 -0
  206. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  207. package/src/wirings/variable/validate-variable-definitions.ts +2 -0
  208. package/src/wirings/variable/variable.types.ts +14 -0
  209. package/src/wirings/workflow/dsl/index.ts +4 -0
  210. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +97 -6
  211. package/src/wirings/workflow/graph/graph-node.ts +2 -0
  212. package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
  213. package/src/wirings/workflow/graph/graph-runner.ts +3 -0
  214. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  215. package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
  216. package/src/wirings/workflow/index.ts +5 -0
  217. package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
  218. package/src/wirings/workflow/pikku-workflow-service.ts +272 -0
  219. package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
  220. package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
  221. package/src/wirings/workflow/workflow.types.ts +4 -2
  222. package/tsconfig.tsbuildinfo +1 -1
  223. package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
  224. package/src/wirings/oauth2/oauth2-client.ts +0 -331
  225. package/src/wirings/oauth2/oauth2-routes.ts +0 -234
@@ -36,6 +36,18 @@ export const clearChannelMiddlewareCache = () => {
36
36
  }
37
37
  }
38
38
 
39
+ /**
40
+ * Combine the inherited (tag/named) channel middleware with any per-run
41
+ * middleware for a wiring.
42
+ *
43
+ * Only the statically-resolved inherited middleware is deterministic per `uid`
44
+ * and safe to cache. `wireChannelMiddleware` is a per-run set of closures (e.g.
45
+ * an AI agent's per-invocation stream middleware holding that run's
46
+ * thread/session state) and MUST NOT be cached — caching it lets a later run of
47
+ * the same `uid` reuse an earlier run's closures, leaking that run's state (and
48
+ * growing memory) across invocations. It is therefore appended fresh on every
49
+ * call after the cached inherited slice.
50
+ */
39
51
  export const combineChannelMiddleware = (
40
52
  wireType: string,
41
53
  uid: string,
@@ -50,40 +62,40 @@ export const combineChannelMiddleware = (
50
62
  } = {}
51
63
  ): readonly CorePikkuChannelMiddleware[] => {
52
64
  const cacheKey = `${wireType}:${uid}`
53
- if (channelMiddlewareCache[cacheKey]) {
54
- return channelMiddlewareCache[cacheKey]
55
- }
56
-
57
- const resolved: CorePikkuChannelMiddleware[] = []
58
-
59
- if (wireInheritedChannelMiddleware) {
60
- for (const meta of wireInheritedChannelMiddleware) {
61
- if (meta.type === 'tag') {
62
- const groups = getTagGroups(
63
- pikkuState(packageName, 'channelMiddleware', 'tagGroup'),
64
- meta.tag
65
- )
66
- for (const group of groups) {
67
- resolved.push(...group)
68
- }
69
- } else if (meta.type === 'wire') {
70
- const middleware = getChannelMiddlewareByName(meta.name)
71
- if (middleware) {
72
- resolved.push(middleware)
65
+ let inherited = channelMiddlewareCache[cacheKey]
66
+ if (!inherited) {
67
+ const resolved: CorePikkuChannelMiddleware[] = []
68
+ if (wireInheritedChannelMiddleware) {
69
+ for (const meta of wireInheritedChannelMiddleware) {
70
+ if (meta.type === 'tag') {
71
+ const groups = getTagGroups(
72
+ pikkuState(packageName, 'channelMiddleware', 'tagGroup'),
73
+ meta.tag
74
+ )
75
+ for (const group of groups) {
76
+ resolved.push(...group)
77
+ }
78
+ } else if (meta.type === 'wire') {
79
+ const middleware = getChannelMiddlewareByName(meta.name)
80
+ if (middleware) {
81
+ resolved.push(middleware)
82
+ }
73
83
  }
74
84
  }
75
85
  }
86
+ inherited = channelMiddlewareCache[cacheKey] = freezeDedupe(
87
+ resolved
88
+ ) as readonly CorePikkuChannelMiddleware[]
76
89
  }
77
90
 
78
- if (wireChannelMiddleware) {
79
- resolved.push(...wireChannelMiddleware)
91
+ if (!wireChannelMiddleware?.length) {
92
+ return inherited
80
93
  }
81
94
 
82
- channelMiddlewareCache[cacheKey] = freezeDedupe(
83
- resolved
84
- ) as readonly CorePikkuChannelMiddleware[]
85
-
86
- return channelMiddlewareCache[cacheKey]
95
+ return freezeDedupe([
96
+ ...inherited,
97
+ ...wireChannelMiddleware,
98
+ ]) as readonly CorePikkuChannelMiddleware[]
87
99
  }
88
100
 
89
101
  export function wrapChannelWithMiddleware<Out>(
@@ -6,7 +6,6 @@ import type {
6
6
  import type {
7
7
  CorePikkuMiddleware,
8
8
  MiddlewareMetadata,
9
- PermissionMetadata,
10
9
  } from '../../types/core.types.js'
11
10
 
12
11
  export type BinaryData = ArrayBuffer | Uint8Array
@@ -23,7 +22,6 @@ export type CorePikkuChannelMiddlewareFactory<
23
22
  Event = unknown,
24
23
  > = (input: In) => CorePikkuChannelMiddleware<Services, Event>
25
24
  import type {
26
- CorePermissionGroup,
27
25
  CorePikkuFunction,
28
26
  CorePikkuFunctionConfig,
29
27
  CorePikkuFunctionSessionless,
@@ -52,7 +50,6 @@ export interface ChannelMessageMeta {
52
50
  errors?: string[]
53
51
  tags?: string[]
54
52
  middleware?: MiddlewareMetadata[]
55
- permissions?: PermissionMetadata[]
56
53
  }
57
54
 
58
55
  export interface ChannelMeta {
@@ -72,7 +69,6 @@ export interface ChannelMeta {
72
69
  errors?: string[]
73
70
  tags?: string[]
74
71
  middleware?: MiddlewareMetadata[] // Pre-resolved middleware chain (tag + explicit)
75
- permissions?: PermissionMetadata[] // Pre-resolved permission chain (tag + explicit)
76
72
  channelMiddleware?: MiddlewareMetadata[]
77
73
  }
78
74
 
@@ -123,7 +119,6 @@ export type CoreChannel<
123
119
  | ChannelFunctionMessage
124
120
  | {
125
121
  func: ChannelFunctionMessage
126
- permissions?: CorePermissionGroup<PikkuPermission>
127
122
  auth?: boolean
128
123
  middleware?: PikkuMiddleware[]
129
124
  }
@@ -133,7 +128,6 @@ export type CoreChannel<
133
128
  channelMiddleware?: Array<
134
129
  CorePikkuChannelMiddleware | CorePikkuChannelMiddlewareFactory
135
130
  >
136
- permissions?: CorePermissionGroup<PikkuPermission>
137
131
  auth?: boolean
138
132
  binary?: boolean | null
139
133
  onBinaryMessage?: (
@@ -374,8 +374,6 @@ export async function runCLICommand({
374
374
  auth: false,
375
375
  inheritedMiddleware: currentCommand.middleware,
376
376
  wireMiddleware: allWireMiddleware,
377
- inheritedPermissions: currentCommand.permissions,
378
- wirePermissions: undefined,
379
377
  coerceDataFromSchema: true,
380
378
  tags: programData?.tags,
381
379
  wire,
@@ -492,6 +490,10 @@ export async function executeCLI({
492
490
  return
493
491
  }
494
492
 
493
+ // Non-fatal diagnostics (unknown options are still accepted) go to stderr
494
+ // so they never pollute a command's machine-readable stdout.
495
+ parsed.warnings.forEach((warning) => console.error(`Warning: ${warning}`))
496
+
495
497
  if (parsed.errors.length > 0) {
496
498
  // Check if any error is about an unknown command
497
499
  const hasUnknownCommand = parsed.errors.some(
@@ -4,7 +4,6 @@ import type {
4
4
  CoreUserSession,
5
5
  CoreServices,
6
6
  MiddlewareMetadata,
7
- PermissionMetadata,
8
7
  } from '../../types/core.types.js'
9
8
  import type {
10
9
  CorePikkuFunctionConfig,
@@ -80,7 +79,6 @@ export interface CLICommandMeta {
80
79
  tags?: string[]
81
80
  subcommands?: Record<string, CLICommandMeta>
82
81
  middleware?: MiddlewareMetadata[] // Pre-resolved middleware chain (tag + explicit)
83
- permissions?: PermissionMetadata[] // Pre-resolved permission chain (tag + explicit)
84
82
  isDefault?: boolean
85
83
  }
86
84
 
@@ -270,6 +270,7 @@ describe('Command Parser', () => {
270
270
  // Unknown options are allowed (for forward compatibility)
271
271
  // They just won't have defaults or validation
272
272
  assert.strictEqual(result.options.unknown, true)
273
+ assert.strictEqual(result.errors.length, 0)
273
274
  })
274
275
 
275
276
  test('should report error for unknown short flag', () => {
@@ -525,4 +526,133 @@ describe('Command Parser', () => {
525
526
  )
526
527
  })
527
528
  })
529
+
530
+ describe('unknown long options', () => {
531
+ const listMeta: CLIMeta = {
532
+ programs: {
533
+ 'test-cli': {
534
+ program: 'test-cli',
535
+ options: {},
536
+ commands: {
537
+ list: {
538
+ pikkuFuncId: 'listFunc',
539
+ positionals: [],
540
+ options: {
541
+ section: { description: 'Section to list', default: 'all' },
542
+ autoApply: {
543
+ description: 'Apply automatically',
544
+ default: false,
545
+ },
546
+ },
547
+ },
548
+ },
549
+ },
550
+ },
551
+ renderers: {},
552
+ }
553
+
554
+ test('warns (but does not error) on an unknown --opt value option', () => {
555
+ const result = parseCLIArguments(
556
+ ['list', '--sektion', 'functions'],
557
+ 'test-cli',
558
+ listMeta
559
+ )
560
+
561
+ assert.strictEqual(result.errors.length, 0, 'should stay non-fatal')
562
+ assert.strictEqual(result.warnings.length, 1)
563
+ assert.ok(
564
+ result.warnings[0].startsWith('Unknown option: --sektion (ignored)'),
565
+ `unexpected warning: ${result.warnings[0]}`
566
+ )
567
+ // Forward compatibility: the value is still parsed through
568
+ assert.strictEqual(result.options.sektion, 'functions')
569
+ // ...and the real option keeps its default
570
+ assert.strictEqual(result.options.section, 'all')
571
+ })
572
+
573
+ test('warns on an unknown --opt=value option', () => {
574
+ const result = parseCLIArguments(
575
+ ['list', '--sektion=functions'],
576
+ 'test-cli',
577
+ listMeta
578
+ )
579
+
580
+ assert.strictEqual(result.errors.length, 0)
581
+ assert.strictEqual(result.warnings.length, 1)
582
+ assert.ok(
583
+ result.warnings[0].startsWith('Unknown option: --sektion (ignored)'),
584
+ `unexpected warning: ${result.warnings[0]}`
585
+ )
586
+ assert.strictEqual(result.options.sektion, 'functions')
587
+ })
588
+
589
+ test('suggests a near-miss option name', () => {
590
+ const result = parseCLIArguments(
591
+ ['list', '--sektion'],
592
+ 'test-cli',
593
+ listMeta
594
+ )
595
+
596
+ assert.ok(
597
+ result.warnings[0].includes('Did you mean --section?'),
598
+ `expected a suggestion, got: ${result.warnings[0]}`
599
+ )
600
+ })
601
+
602
+ test('suggests the kebab-case rendering of a camelCase option', () => {
603
+ const result = parseCLIArguments(
604
+ ['list', '--auto-aply'],
605
+ 'test-cli',
606
+ listMeta
607
+ )
608
+
609
+ assert.ok(
610
+ result.warnings[0].includes('Did you mean --auto-apply?'),
611
+ `expected a suggestion, got: ${result.warnings[0]}`
612
+ )
613
+ })
614
+
615
+ test('omits the suggestion when nothing is close', () => {
616
+ const result = parseCLIArguments(
617
+ ['list', '--completely-different'],
618
+ 'test-cli',
619
+ listMeta
620
+ )
621
+
622
+ assert.strictEqual(
623
+ result.warnings[0],
624
+ 'Unknown option: --completely-different (ignored)'
625
+ )
626
+ })
627
+
628
+ test('does not warn for a known option (either casing)', () => {
629
+ const kebab = parseCLIArguments(
630
+ ['list', '--section', 'functions', '--auto-apply'],
631
+ 'test-cli',
632
+ listMeta
633
+ )
634
+ assert.deepStrictEqual(kebab.warnings, [])
635
+ assert.strictEqual(kebab.options.section, 'functions')
636
+
637
+ const camel = parseCLIArguments(
638
+ ['list', '--autoApply'],
639
+ 'test-cli',
640
+ listMeta
641
+ )
642
+ assert.deepStrictEqual(camel.warnings, [])
643
+ })
644
+
645
+ test('does not warn for --help', () => {
646
+ const result = parseCLIArguments(['list', '--help'], 'test-cli', listMeta)
647
+
648
+ assert.deepStrictEqual(result.warnings, [])
649
+ })
650
+
651
+ test('unknown short flags still error, not warn', () => {
652
+ const result = parseCLIArguments(['list', '-x'], 'test-cli', listMeta)
653
+
654
+ assert.ok(result.errors.some((e) => e.includes('Unknown option: -x')))
655
+ assert.deepStrictEqual(result.warnings, [])
656
+ })
657
+ })
528
658
  })
@@ -18,6 +18,71 @@ function toKebabCase(str: string): string {
18
18
  return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
19
19
  }
20
20
 
21
+ /** Options the runner handles itself — never reported as unknown. */
22
+ const RESERVED_OPTIONS = new Set(['help'])
23
+
24
+ /** Levenshtein distance, capped-free and dependency-free. Used only to suggest
25
+ * a near-miss option name, so the naive O(n*m) implementation is fine. */
26
+ function levenshtein(a: string, b: string): number {
27
+ if (a === b) return 0
28
+ if (a.length === 0) return b.length
29
+ if (b.length === 0) return a.length
30
+
31
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i)
32
+ for (let i = 1; i <= a.length; i++) {
33
+ const row = [i]
34
+ for (let j = 1; j <= b.length; j++) {
35
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1
36
+ row[j] = Math.min(row[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost)
37
+ }
38
+ prev = row
39
+ }
40
+ return prev[b.length]
41
+ }
42
+
43
+ /** Finds the closest declared option (distance <= 2) to what the user typed.
44
+ * Compares against the kebab-case rendering, since that is what is typed. */
45
+ function suggestOption(
46
+ typed: string,
47
+ availableOptions: Record<string, CLIOption>
48
+ ): string | null {
49
+ let best: string | null = null
50
+ let bestDistance = 3
51
+
52
+ for (const name of Object.keys(availableOptions)) {
53
+ const kebab = toKebabCase(name)
54
+ const distance = Math.min(
55
+ levenshtein(typed, kebab),
56
+ levenshtein(typed, name)
57
+ )
58
+ if (distance < bestDistance) {
59
+ bestDistance = distance
60
+ best = kebab
61
+ }
62
+ }
63
+
64
+ return best
65
+ }
66
+
67
+ /** Records a warning that an unknown long option was accepted but ignored.
68
+ * Unknown options stay non-fatal for forward compatibility (a newer command
69
+ * version may understand them) — they are just no longer silent. */
70
+ function warnUnknownOption(
71
+ typed: string,
72
+ availableOptions: Record<string, CLIOption>,
73
+ result: ParsedCommand
74
+ ) {
75
+ if (RESERVED_OPTIONS.has(toCamelCase(typed))) {
76
+ return
77
+ }
78
+
79
+ const suggestion = suggestOption(typed, availableOptions)
80
+ result.warnings.push(
81
+ `Unknown option: --${typed} (ignored)` +
82
+ (suggestion ? ` Did you mean --${suggestion}?` : '')
83
+ )
84
+ }
85
+
21
86
  /**
22
87
  * Result of parsing CLI arguments
23
88
  */
@@ -27,6 +92,8 @@ export interface ParsedCommand {
27
92
  positionals: Record<string, any>
28
93
  options: Record<string, any>
29
94
  errors: string[]
95
+ /** Non-fatal diagnostics (e.g. unknown options that were accepted+ignored) */
96
+ warnings: string[]
30
97
  }
31
98
 
32
99
  /**
@@ -43,6 +110,7 @@ export function parseCLIArguments(
43
110
  positionals: {},
44
111
  options: {},
45
112
  errors: [],
113
+ warnings: [],
46
114
  }
47
115
 
48
116
  const meta = allMeta.programs[programName]
@@ -148,7 +216,12 @@ export function parseCLIArguments(
148
216
  const key = toCamelCase(arg.slice(2, equalIndex))
149
217
  const optionDef = availableOptions[key]
150
218
 
151
- // Unknown options are allowed for forward compatibility
219
+ // Unknown options are allowed for forward compatibility, but warned
220
+ // about so they are not silently dropped by the input schema.
221
+ if (!optionDef) {
222
+ warnUnknownOption(arg.slice(2, equalIndex), availableOptions, result)
223
+ }
224
+
152
225
  const value = arg.slice(equalIndex + 1)
153
226
  optionArgs[key] = parseOptionValue(value, optionDef)
154
227
  } else {
@@ -156,7 +229,12 @@ export function parseCLIArguments(
156
229
  const key = toCamelCase(arg.slice(2))
157
230
  const optionDef = availableOptions[key]
158
231
 
159
- // Unknown options are allowed for forward compatibility
232
+ // Unknown options are allowed for forward compatibility, but warned
233
+ // about so they are not silently dropped by the input schema.
234
+ if (!optionDef) {
235
+ warnUnknownOption(arg.slice(2), availableOptions, result)
236
+ }
237
+
160
238
  if (optionDef && optionDef.array) {
161
239
  // Array option - collect all following non-flag values
162
240
  currentIndex++
@@ -6,6 +6,13 @@ export type CoreCredential<T = unknown> = {
6
6
  description?: string
7
7
  type: 'singleton' | 'wire'
8
8
  schema: T
9
+ /**
10
+ * Link to documentation explaining how to obtain this value — a provider's
11
+ * API-key page, a setup guide, an internal runbook. Surfaced by consoles and
12
+ * deploy UIs so a user facing a missing value has somewhere to go instead of
13
+ * an opaque identifier.
14
+ */
15
+ docsUrl?: string
9
16
  oauth2?: OAuth2CredentialConfig & {
10
17
  appCredentialSecretId: string
11
18
  }
@@ -17,6 +24,13 @@ export type CredentialDefinitionMeta = {
17
24
  description?: string
18
25
  type: 'singleton' | 'wire'
19
26
  schema?: Record<string, unknown> | string
27
+ /**
28
+ * Link to documentation explaining how to obtain this value — a provider's
29
+ * API-key page, a setup guide, an internal runbook. Surfaced by consoles and
30
+ * deploy UIs so a user facing a missing value has somewhere to go instead of
31
+ * an opaque identifier.
32
+ */
33
+ docsUrl?: string
20
34
  oauth2?: OAuth2CredentialConfig & {
21
35
  appCredentialSecretId: string
22
36
  }
@@ -56,6 +56,7 @@ export function validateAndBuildCredentialDefinitionsMeta(
56
56
  type: def.type,
57
57
  schema: def.schema,
58
58
  oauth2: def.oauth2,
59
+ docsUrl: def.docsUrl,
59
60
  sourceFile: def.sourceFile,
60
61
  }
61
62
  }