@pikku/cli 0.12.144 → 0.12.145

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 (136) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/.pikku/addon/index.d.ts +1 -1
  3. package/dist/.pikku/addon/index.js +1 -1
  4. package/dist/.pikku/addon/pikku-addon-types.gen.d.ts +1 -1
  5. package/dist/.pikku/addon/pikku-addon-types.gen.js +1 -1
  6. package/dist/.pikku/agent/index.d.ts +1 -1
  7. package/dist/.pikku/agent/index.js +1 -1
  8. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  9. package/dist/.pikku/agent/pikku-model-aliases.gen.js +1 -1
  10. package/dist/.pikku/auth/index.d.ts +1 -1
  11. package/dist/.pikku/auth/index.js +1 -1
  12. package/dist/.pikku/auth/pikku-auth-types.gen.d.ts +1 -1
  13. package/dist/.pikku/auth/pikku-auth-types.gen.js +1 -1
  14. package/dist/.pikku/channel/index.d.ts +1 -1
  15. package/dist/.pikku/channel/index.js +1 -1
  16. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  17. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  18. package/dist/.pikku/cli/index.d.ts +1 -1
  19. package/dist/.pikku/cli/index.js +1 -1
  20. package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
  21. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  22. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  23. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  24. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  25. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  26. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  27. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  28. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  29. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  30. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  31. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  32. package/dist/.pikku/error/index.d.ts +1 -1
  33. package/dist/.pikku/error/index.js +1 -1
  34. package/dist/.pikku/error/pikku-error-types.gen.d.ts +1 -1
  35. package/dist/.pikku/error/pikku-error-types.gen.js +1 -1
  36. package/dist/.pikku/function/index.d.ts +1 -1
  37. package/dist/.pikku/function/index.js +1 -1
  38. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  39. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  40. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  41. package/dist/.pikku/function/pikku-functions-meta.gen.json +2 -2
  42. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  43. package/dist/.pikku/gateway/index.d.ts +1 -1
  44. package/dist/.pikku/gateway/index.js +1 -1
  45. package/dist/.pikku/gateway/pikku-gateway-types.gen.d.ts +1 -1
  46. package/dist/.pikku/gateway/pikku-gateway-types.gen.js +1 -1
  47. package/dist/.pikku/http/index.d.ts +1 -1
  48. package/dist/.pikku/http/index.js +1 -1
  49. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  50. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  51. package/dist/.pikku/mcp/index.d.ts +1 -1
  52. package/dist/.pikku/mcp/index.js +1 -1
  53. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  54. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  55. package/dist/.pikku/middleware/index.d.ts +1 -1
  56. package/dist/.pikku/middleware/index.js +1 -1
  57. package/dist/.pikku/middleware/pikku-middleware-types.gen.d.ts +1 -1
  58. package/dist/.pikku/middleware/pikku-middleware-types.gen.js +1 -1
  59. package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +1 -1
  60. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +1 -1
  61. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  62. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  63. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  64. package/dist/.pikku/queue/index.d.ts +1 -1
  65. package/dist/.pikku/queue/index.js +1 -1
  66. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  67. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  68. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  69. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  70. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  71. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  72. package/dist/.pikku/scenarios/index.d.ts +1 -1
  73. package/dist/.pikku/scenarios/index.js +1 -1
  74. package/dist/.pikku/scenarios/pikku-personas.gen.js +1 -1
  75. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
  76. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
  77. package/dist/.pikku/scenarios/pikku-scenario-types.gen.d.ts +1 -1
  78. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
  79. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
  80. package/dist/.pikku/scenarios/schemas/register.gen.d.ts +1 -1
  81. package/dist/.pikku/scenarios/schemas/register.gen.js +1 -1
  82. package/dist/.pikku/scheduler/index.d.ts +1 -1
  83. package/dist/.pikku/scheduler/index.js +1 -1
  84. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  85. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  86. package/dist/.pikku/schemas/register.gen.js +1 -1
  87. package/dist/.pikku/scopes/index.d.ts +1 -1
  88. package/dist/.pikku/scopes/index.js +1 -1
  89. package/dist/.pikku/scopes/pikku-personas.gen.js +1 -1
  90. package/dist/.pikku/scopes/pikku-roles.gen.d.ts +1 -1
  91. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
  92. package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
  93. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  94. package/dist/.pikku/secrets/index.d.ts +1 -1
  95. package/dist/.pikku/secrets/index.js +1 -1
  96. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  97. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  98. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  99. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  100. package/dist/.pikku/services/pikku-meta-service.gen.d.ts +1 -1
  101. package/dist/.pikku/services/pikku-meta-service.gen.js +1 -1
  102. package/dist/.pikku/setup/index.d.ts +1 -1
  103. package/dist/.pikku/setup/index.js +1 -1
  104. package/dist/.pikku/setup/pikku-setup-types.gen.d.ts +1 -1
  105. package/dist/.pikku/setup/pikku-setup-types.gen.js +1 -1
  106. package/dist/.pikku/trigger/index.d.ts +1 -1
  107. package/dist/.pikku/trigger/index.js +1 -1
  108. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  109. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  110. package/dist/.pikku/variables/index.d.ts +1 -1
  111. package/dist/.pikku/variables/index.js +1 -1
  112. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  113. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  114. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  115. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  116. package/dist/.pikku/workflow/index.d.ts +1 -1
  117. package/dist/.pikku/workflow/index.js +1 -1
  118. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  119. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  120. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  121. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  122. package/dist/bin/pikku-bin.mjs +2 -2
  123. package/dist/src/deploy/analyzer/analyzer.d.ts +8 -1
  124. package/dist/src/deploy/analyzer/analyzer.js +173 -51
  125. package/dist/src/deploy/analyzer/grouping.d.ts +23 -0
  126. package/dist/src/deploy/analyzer/grouping.js +74 -0
  127. package/dist/src/deploy/analyzer/index.d.ts +1 -0
  128. package/dist/src/deploy/analyzer/naming.d.ts +1 -0
  129. package/dist/src/deploy/analyzer/naming.js +9 -0
  130. package/dist/src/deploy/build-pipeline.d.ts +2 -0
  131. package/dist/src/deploy/build-pipeline.js +20 -0
  132. package/dist/src/functions/commands/deploy-apply.js +1 -0
  133. package/dist/src/functions/commands/deploy-plan.js +1 -0
  134. package/dist/tsconfig.tsbuildinfo +1 -1
  135. package/package.json +1 -1
  136. package/surface.json +1 -1
@@ -9,6 +9,8 @@
9
9
  import { resolveDeployTarget, } from '@pikku/inspector';
10
10
  import { relative } from 'node:path';
11
11
  import { isScenarioFunction, withoutScenarios, withoutScenarioWorkflows, } from '../../functions/wirings/scenarios/scenario-partition.js';
12
+ import { toSafeKebab } from './naming.js';
13
+ import { createUnitResolver } from './grouping.js';
12
14
  export function analyzeDeployment(state, options) {
13
15
  const serverlessIncompatible = new Set(options.serverlessIncompatible ?? []);
14
16
  const defaultTarget = options.defaultTarget ?? 'serverless';
@@ -30,6 +32,86 @@ export function analyzeDeployment(state, options) {
30
32
  const functionsMeta = withoutScenarios(state.functions.meta);
31
33
  const graphMeta = withoutScenarioWorkflows(state.workflows.graphMeta);
32
34
  const httpMeta = state.http.meta;
35
+ const tagIndex = buildFunctionTagIndex(state, functionsMeta);
36
+ const tagsFor = (funcId) => tagIndex.get(funcId) ?? [];
37
+ const resolver = createUnitResolver(options.grouping, {
38
+ routesForFunction: (funcId) => collectHttpRoutes(httpMeta, funcId).flatMap((r) => [
39
+ r.route,
40
+ prefixed(r.route),
41
+ ]),
42
+ tagsForFunction: tagsFor,
43
+ });
44
+ const addFunctionUnit = (unit) => {
45
+ const existing = units.find((u) => u.name === unit.name);
46
+ if (!existing) {
47
+ units.push(unit);
48
+ return;
49
+ }
50
+ if (existing.target !== unit.target) {
51
+ const rule = resolver.ruleForUnit(unit.name);
52
+ throw new Error(`deploy.grouping: unit "${unit.name}" would hold both serverless and server functions ` +
53
+ `(${existing.functionIds.join(', ')} vs ${unit.functionIds.join(', ')}). ` +
54
+ (rule
55
+ ? `Give the server-target functions their own rule, or drop them from "${unit.name}".`
56
+ : `A grouping rule cannot change a function's target.`));
57
+ }
58
+ for (const id of unit.functionIds) {
59
+ if (!existing.functionIds.includes(id)) {
60
+ existing.functionIds.push(id);
61
+ }
62
+ }
63
+ for (const service of unit.services) {
64
+ if (!existing.services.some((s) => s.capability === service.capability &&
65
+ s.sourceServiceName === service.sourceServiceName)) {
66
+ existing.services.push(service);
67
+ }
68
+ }
69
+ for (const dep of unit.dependsOn) {
70
+ if (dep !== existing.name && !existing.dependsOn.includes(dep)) {
71
+ existing.dependsOn.push(dep);
72
+ }
73
+ }
74
+ for (const tag of unit.tags) {
75
+ if (!existing.tags.includes(tag)) {
76
+ existing.tags.push(tag);
77
+ }
78
+ }
79
+ for (const handler of unit.handlers) {
80
+ if (handler.type === 'fetch') {
81
+ const fetchHandler = existing.handlers.find((h) => h.type === 'fetch');
82
+ if (fetchHandler) {
83
+ fetchHandler.routes.push(...handler.routes);
84
+ }
85
+ else {
86
+ existing.handlers.push(handler);
87
+ }
88
+ }
89
+ else {
90
+ existing.handlers.push(handler);
91
+ }
92
+ }
93
+ if (unit.invokedAgents?.length) {
94
+ const merged = new Set([
95
+ ...(existing.invokedAgents ?? []),
96
+ ...unit.invokedAgents,
97
+ ]);
98
+ existing.invokedAgents = [...merged];
99
+ }
100
+ };
101
+ const unitFunctionIds = new Map();
102
+ const unitFor = (funcId) => {
103
+ const name = resolver.forFunction(funcId);
104
+ const existing = unitFunctionIds.get(name);
105
+ if (existing) {
106
+ if (!existing.includes(funcId)) {
107
+ existing.push(funcId);
108
+ }
109
+ }
110
+ else {
111
+ unitFunctionIds.set(name, [funcId]);
112
+ }
113
+ return name;
114
+ };
33
115
  // ── Step 1: Create function units ──────────────────────────────────
34
116
  // Each function gets one unit. Collect all its triggers.
35
117
  for (const [funcId, funcMeta] of entries(functionsMeta)) {
@@ -70,7 +152,7 @@ export function analyzeDeployment(state, options) {
70
152
  handlers.push({ type: 'queue', queueName: queueMeta.name ?? queueName });
71
153
  queues.push({
72
154
  name: queueMeta.name ?? queueName,
73
- consumerUnit: toSafeKebab(funcId),
155
+ consumerUnit: unitFor(funcId),
74
156
  consumerFunctionId: funcId,
75
157
  });
76
158
  }
@@ -86,7 +168,7 @@ export function analyzeDeployment(state, options) {
86
168
  scheduledTasks.push({
87
169
  name: schedMeta.name,
88
170
  schedule: schedMeta.schedule,
89
- unitName: toSafeKebab(funcId),
171
+ unitName: unitFor(funcId),
90
172
  functionId: funcId,
91
173
  });
92
174
  }
@@ -128,15 +210,15 @@ export function analyzeDeployment(state, options) {
128
210
  continue;
129
211
  }
130
212
  const invokedAgents = collectInvokedAgents(state, funcId);
131
- units.push({
132
- name: toSafeKebab(funcId),
213
+ addFunctionUnit({
214
+ name: unitFor(funcId),
133
215
  role: 'function',
134
216
  target: resolveDeployTarget(funcMeta, serverlessIncompatible, funcId, defaultTarget),
135
217
  functionIds: [funcId],
136
218
  services: withAgentServices(collectServicesForFunction(funcMeta), invokedAgents),
137
219
  dependsOn: [],
138
220
  handlers,
139
- tags: funcMeta.tags ?? [],
221
+ tags: tagsFor(funcId),
140
222
  ...(invokedAgents.length > 0 && { invokedAgents }),
141
223
  });
142
224
  }
@@ -147,7 +229,7 @@ export function analyzeDeployment(state, options) {
147
229
  if (exposed.length === 0) {
148
230
  continue;
149
231
  }
150
- const unitName = `addon-${toSafeKebab(namespace)}`;
232
+ const unitName = resolver.forAddon(namespace);
151
233
  const addonIncompatible = new Set(state.addonServerlessIncompatible?.get(namespace) ?? []);
152
234
  const routes = [];
153
235
  const functionIds = [];
@@ -176,7 +258,7 @@ export function analyzeDeployment(state, options) {
176
258
  target = 'server';
177
259
  }
178
260
  }
179
- units.push({
261
+ addFunctionUnit({
180
262
  name: unitName,
181
263
  role: 'function',
182
264
  target,
@@ -193,7 +275,7 @@ export function analyzeDeployment(state, options) {
193
275
  const subAgentNames = agentMeta.agents ?? [];
194
276
  const unitName = `agent-${toSafeKebab(agentName)}`;
195
277
  // Agent gateway depends on its tool function units
196
- const toolUnitNames = toolIds.map((id) => toSafeKebab(id));
278
+ const toolUnitNames = toolIds.map((id) => unitFor(id));
197
279
  const subAgentUnitNames = subAgentNames.map((sa) => `agent-${toSafeKebab(sa)}`);
198
280
  // Agent needs AI services
199
281
  const agentServices = agentServiceRequirements();
@@ -271,7 +353,7 @@ export function analyzeDeployment(state, options) {
271
353
  const allMcpIds = [...mcpToolIds, ...mcpResourceIds, ...mcpPromptIds];
272
354
  if (allMcpIds.length > 0) {
273
355
  const unitName = 'mcp-server';
274
- const mcpFuncUnitNames = allMcpIds.map((id) => toSafeKebab(id));
356
+ const mcpFuncUnitNames = allMcpIds.map((id) => unitFor(id));
275
357
  units.push({
276
358
  name: unitName,
277
359
  role: 'mcp',
@@ -280,7 +362,7 @@ export function analyzeDeployment(state, options) {
280
362
  services: [],
281
363
  dependsOn: mcpFuncUnitNames,
282
364
  handlers: [{ type: 'fetch', routes: [] }],
283
- tags: collectTags(allMcpIds, functionsMeta),
365
+ tags: collectTags(allMcpIds, tagsFor),
284
366
  });
285
367
  mcpEndpoints.push({
286
368
  unitName,
@@ -295,7 +377,7 @@ export function analyzeDeployment(state, options) {
295
377
  if (funcIds.length === 0)
296
378
  continue;
297
379
  const unitName = `channel-${toSafeKebab(channelName)}`;
298
- const funcUnitNames = funcIds.map((id) => toSafeKebab(id));
380
+ const funcUnitNames = funcIds.map((id) => unitFor(id));
299
381
  units.push({
300
382
  name: unitName,
301
383
  role: 'channel',
@@ -314,34 +396,35 @@ export function analyzeDeployment(state, options) {
314
396
  });
315
397
  }
316
398
  // ── Step 5: Workflows ──────────────────────────────────────────────
317
- buildWorkflows(graphMeta, functionsMeta, { ...state.rpc?.exposedMeta, ...state.rpc?.internalMeta }, httpMeta, units, workflows, queues, workflowQueues);
399
+ buildWorkflows(graphMeta, functionsMeta, { ...state.rpc?.exposedMeta, ...state.rpc?.internalMeta }, httpMeta, units, workflows, queues, workflowQueues, unitFor);
318
400
  // ── Step 6: Ensure function units exist for gateway dependencies ───
319
401
  // Gateways depend on function units. If a function is only used via
320
402
  // a gateway (not directly wired to HTTP/queue/cron), it still needs
321
403
  // a unit with a fetch handler for RPC access.
322
- const existingUnitNames = new Set(units.map((u) => u.name));
323
404
  const unitsSnapshot = Array.from(units);
324
405
  for (const unit of unitsSnapshot) {
325
406
  for (const dep of unit.dependsOn) {
326
- if (!existingUnitNames.has(dep)) {
327
- // Find the function ID for this dependency
328
- const funcId = fromKebab(dep);
407
+ for (const funcId of unitFunctionIds.get(dep) ?? []) {
408
+ const existing = units.find((u) => u.name === dep);
409
+ if (existing?.functionIds.includes(funcId)) {
410
+ continue;
411
+ }
329
412
  const funcMeta = functionsMeta[funcId];
330
- if (funcMeta) {
331
- const invokedAgents = collectInvokedAgents(state, funcId);
332
- units.push({
333
- name: dep,
334
- role: 'function',
335
- target: resolveDeployTarget(funcMeta, serverlessIncompatible, funcId, defaultTarget),
336
- functionIds: [funcId],
337
- services: withAgentServices(collectServicesForFunction(funcMeta), invokedAgents),
338
- dependsOn: [],
339
- handlers: [{ type: 'fetch', routes: [] }],
340
- tags: funcMeta.tags ?? [],
341
- ...(invokedAgents.length > 0 && { invokedAgents }),
342
- });
343
- existingUnitNames.add(dep);
413
+ if (!funcMeta) {
414
+ continue;
344
415
  }
416
+ const invokedAgents = collectInvokedAgents(state, funcId);
417
+ addFunctionUnit({
418
+ name: dep,
419
+ role: 'function',
420
+ target: resolveDeployTarget(funcMeta, serverlessIncompatible, funcId, defaultTarget),
421
+ functionIds: [funcId],
422
+ services: withAgentServices(collectServicesForFunction(funcMeta), invokedAgents),
423
+ dependsOn: [],
424
+ handlers: [{ type: 'fetch', routes: [] }],
425
+ tags: tagsFor(funcId),
426
+ ...(invokedAgents.length > 0 && { invokedAgents }),
427
+ });
345
428
  }
346
429
  }
347
430
  }
@@ -557,7 +640,7 @@ export function analyzeDeployment(state, options) {
557
640
  // ---------------------------------------------------------------------------
558
641
  // Workflow builder
559
642
  // ---------------------------------------------------------------------------
560
- function buildWorkflows(graphMeta, functionsMeta, rpcMeta, _httpMeta, units, workflows, queues, workflowQueues) {
643
+ function buildWorkflows(graphMeta, functionsMeta, rpcMeta, _httpMeta, units, workflows, queues, workflowQueues, unitFor) {
561
644
  for (const [_wfName, graph] of entries(graphMeta)) {
562
645
  const steps = [];
563
646
  const stepUnitNames = [];
@@ -566,7 +649,7 @@ function buildWorkflows(graphMeta, functionsMeta, rpcMeta, _httpMeta, units, wor
566
649
  continue;
567
650
  if (!('rpcName' in node))
568
651
  continue;
569
- const stepUnitName = toSafeKebab(node.rpcName);
652
+ const stepUnitName = unitFor(node.rpcName);
570
653
  // Step dispatch is decided per-function: a step runs inline unless its
571
654
  // function is marked `workflowQueued: true` (then it dispatches via queue).
572
655
  const stepFuncId = rpcMeta[node.rpcName] ?? node.rpcName;
@@ -654,7 +737,7 @@ function buildWorkflows(graphMeta, functionsMeta, rpcMeta, _httpMeta, units, wor
654
737
  const stepQueueName = `wf-step-${toSafeKebab(step.functionId)}`;
655
738
  queues.push({
656
739
  name: stepQueueName,
657
- consumerUnit: toSafeKebab(step.functionId),
740
+ consumerUnit: unitFor(step.functionId),
658
741
  consumerFunctionId: `pikkuWorkflowWorker:${step.functionId}`,
659
742
  });
660
743
  }
@@ -797,32 +880,71 @@ function collectChannelFunctionIds(channelMeta) {
797
880
  // ---------------------------------------------------------------------------
798
881
  // Naming helpers
799
882
  // ---------------------------------------------------------------------------
800
- export function toSafeKebab(str) {
801
- return str
802
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
803
- .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
804
- .replace(/[:/\\]/g, '-')
805
- .replace(/--+/g, '-')
806
- .replace(/^-|-$/g, '')
807
- .toLowerCase();
808
- }
809
- function fromKebab(str) {
810
- return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
811
- }
883
+ export { toSafeKebab } from './naming.js';
812
884
  // ---------------------------------------------------------------------------
813
885
  // Tag helpers
814
886
  // ---------------------------------------------------------------------------
815
- function collectTags(funcIds, functionsMeta) {
887
+ function collectTags(funcIds, tagsFor) {
816
888
  const tags = new Set();
817
889
  for (const id of funcIds) {
818
- const meta = functionsMeta[id];
819
- if (meta?.tags) {
820
- for (const tag of meta.tags)
821
- tags.add(tag);
822
- }
890
+ for (const tag of tagsFor(id))
891
+ tags.add(tag);
823
892
  }
824
893
  return [...tags];
825
894
  }
895
+ /**
896
+ * Every tag that applies to a function, from the function itself and from every
897
+ * wiring that reaches it.
898
+ *
899
+ * Tags are almost always written on the wiring — `wireHTTP({ tags: ['todos'] })`
900
+ * — not on the `pikkuFunc`, so reading `FunctionMeta.tags` alone sees nothing in
901
+ * a typical project. `deploy.grouping` matches on the tags a user actually
902
+ * wrote, which means the union.
903
+ */
904
+ function buildFunctionTagIndex(state, functionsMeta) {
905
+ const index = new Map();
906
+ const add = (funcId, tags) => {
907
+ if (!funcId || !tags?.length)
908
+ return;
909
+ let set = index.get(funcId);
910
+ if (!set) {
911
+ set = new Set();
912
+ index.set(funcId, set);
913
+ }
914
+ for (const tag of tags)
915
+ set.add(tag);
916
+ };
917
+ for (const [funcId, meta] of entries(functionsMeta)) {
918
+ add(funcId, meta.tags);
919
+ }
920
+ for (const method of HTTP_METHODS) {
921
+ const methodRoutes = state.http?.meta?.[method];
922
+ if (!methodRoutes)
923
+ continue;
924
+ for (const routeMeta of values(methodRoutes)) {
925
+ add(routeMeta.pikkuFuncId, routeMeta.tags);
926
+ }
927
+ }
928
+ for (const meta of values(state.queueWorkers?.meta ?? {})) {
929
+ add(meta.pikkuFuncId, meta.tags);
930
+ }
931
+ for (const meta of values(state.scheduledTasks?.meta ?? {})) {
932
+ add(meta.pikkuFuncId, meta.tags);
933
+ }
934
+ for (const meta of [
935
+ ...values(state.mcpEndpoints?.toolsMeta ?? {}),
936
+ ...values(state.mcpEndpoints?.resourcesMeta ?? {}),
937
+ ...values(state.mcpEndpoints?.promptsMeta ?? {}),
938
+ ]) {
939
+ add(meta.pikkuFuncId, meta.tags);
940
+ }
941
+ for (const channelMeta of values(state.channels?.meta ?? {})) {
942
+ for (const funcId of collectChannelFunctionIds(channelMeta)) {
943
+ add(funcId, channelMeta.tags);
944
+ }
945
+ }
946
+ return new Map([...index].map(([funcId, tags]) => [funcId, [...tags]]));
947
+ }
826
948
  // ---------------------------------------------------------------------------
827
949
  // Typed iteration helpers
828
950
  // ---------------------------------------------------------------------------
@@ -0,0 +1,23 @@
1
+ export type GroupingStrategy = 'function' | 'single';
2
+ export interface GroupingRule {
3
+ unit: string;
4
+ tags?: string[];
5
+ addon?: string;
6
+ routes?: string[];
7
+ }
8
+ export interface GroupingConfig {
9
+ strategy?: GroupingStrategy;
10
+ rules?: GroupingRule[];
11
+ }
12
+ export interface UnitResolverInput {
13
+ routesForFunction: (funcId: string) => string[];
14
+ tagsForFunction: (funcId: string) => string[];
15
+ }
16
+ export interface UnitResolver {
17
+ forFunction: (funcId: string) => string;
18
+ forAddon: (namespace: string) => string;
19
+ isGrouped: boolean;
20
+ ruleForUnit: (unitName: string) => GroupingRule | undefined;
21
+ }
22
+ export declare const validateGrouping: (config: GroupingConfig) => void;
23
+ export declare const createUnitResolver: (config: GroupingConfig | undefined, input: UnitResolverInput) => UnitResolver;
@@ -0,0 +1,74 @@
1
+ import { toSafeKebab } from './naming.js';
2
+ const SINGLE_UNIT_NAME = 'app';
3
+ const routeMatcher = (pattern) => {
4
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
5
+ return new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
6
+ };
7
+ export const validateGrouping = (config) => {
8
+ const rules = config.rules ?? [];
9
+ const seen = new Set();
10
+ for (const rule of rules) {
11
+ if (!rule.unit) {
12
+ throw new Error(`deploy.grouping: every rule needs a "unit" name (offending rule: ${JSON.stringify(rule)})`);
13
+ }
14
+ if (seen.has(rule.unit)) {
15
+ throw new Error(`deploy.grouping: two rules both name the unit "${rule.unit}". Merge them into one rule.`);
16
+ }
17
+ seen.add(rule.unit);
18
+ if (!rule.tags?.length && !rule.addon && !rule.routes?.length) {
19
+ throw new Error(`deploy.grouping: rule "${rule.unit}" matches nothing. Give it at least one of "tags", "addon" or "routes".`);
20
+ }
21
+ }
22
+ };
23
+ export const createUnitResolver = (config, input) => {
24
+ const strategy = config?.strategy ?? 'function';
25
+ const rules = config?.rules ?? [];
26
+ if (config) {
27
+ validateGrouping(config);
28
+ }
29
+ const compiled = rules.map((rule) => ({
30
+ rule,
31
+ unit: toSafeKebab(rule.unit),
32
+ tags: rule.tags,
33
+ addon: rule.addon,
34
+ routes: rule.routes?.map(routeMatcher),
35
+ }));
36
+ const byUnitName = new Map(compiled.map((c) => [c.unit, c.rule]));
37
+ const matches = (c, tags, routes, addon) => {
38
+ if (c.tags && !c.tags.some((tag) => tags.includes(tag))) {
39
+ return false;
40
+ }
41
+ if (c.addon && c.addon !== addon) {
42
+ return false;
43
+ }
44
+ if (c.routes && !c.routes.some((re) => routes.some((r) => re.test(r)))) {
45
+ return false;
46
+ }
47
+ return true;
48
+ };
49
+ const fallback = (funcId) => strategy === 'single' ? SINGLE_UNIT_NAME : toSafeKebab(funcId);
50
+ return {
51
+ isGrouped: strategy !== 'function' || compiled.length > 0,
52
+ ruleForUnit: (unitName) => byUnitName.get(unitName),
53
+ forFunction: (funcId) => {
54
+ if (compiled.length > 0) {
55
+ const tags = input.tagsForFunction(funcId);
56
+ const routes = input.routesForFunction(funcId);
57
+ for (const c of compiled) {
58
+ if (matches(c, tags, routes, undefined)) {
59
+ return c.unit;
60
+ }
61
+ }
62
+ }
63
+ return fallback(funcId);
64
+ },
65
+ forAddon: (namespace) => {
66
+ for (const c of compiled) {
67
+ if (c.addon === namespace && !c.tags && !c.routes) {
68
+ return c.unit;
69
+ }
70
+ }
71
+ return `addon-${toSafeKebab(namespace)}`;
72
+ },
73
+ };
74
+ };
@@ -1,2 +1,3 @@
1
1
  export { analyzeDeployment } from './analyzer.js';
2
2
  export type { AnalyzerOptions } from './analyzer.js';
3
+ export type { GroupingConfig, GroupingRule, GroupingStrategy, } from './grouping.js';
@@ -0,0 +1 @@
1
+ export declare function toSafeKebab(str: string): string;
@@ -0,0 +1,9 @@
1
+ export function toSafeKebab(str) {
2
+ return str
3
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
4
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
5
+ .replace(/[:/\\]/g, '-')
6
+ .replace(/--+/g, '-')
7
+ .replace(/^-|-$/g, '')
8
+ .toLowerCase();
9
+ }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { InspectorState } from '@pikku/inspector';
8
8
  import { PikkuError } from '@pikku/core/errors';
9
+ import type { GroupingConfig } from './analyzer/index.js';
9
10
  import type { DeploymentManifest } from '@pikku/deploy';
10
11
  import type { Bundler } from './bundler/bundler.interface.js';
11
12
  import type { BundleResult } from './bundler/types.js';
@@ -50,6 +51,7 @@ export declare function runBuildPipeline(options: {
50
51
  inspectorState: InspectorState;
51
52
  serverlessIncompatible?: string[];
52
53
  defaultTarget?: 'serverless' | 'server';
54
+ grouping?: GroupingConfig;
53
55
  globalHTTPPrefix?: string;
54
56
  getEntryContext: (unitDir: string, pikkuDir: string, unit: DeploymentManifest['units'][0], state: InspectorState) => unknown;
55
57
  /**
@@ -153,6 +153,7 @@ export async function runBuildPipeline(options) {
153
153
  projectId,
154
154
  serverlessIncompatible: options.serverlessIncompatible,
155
155
  defaultTarget: options.defaultTarget,
156
+ grouping: options.grouping,
156
157
  globalHTTPPrefix: options.globalHTTPPrefix,
157
158
  workflowQueues,
158
159
  });
@@ -299,7 +300,26 @@ export async function runBuildPipeline(options) {
299
300
  unitPikkuDirs.set(k, v);
300
301
  codegenErrors.push(...serverCodegenErrors);
301
302
  // Replace individual server units with the merged one in the manifest
303
+ const mergedNames = new Set(serverUnits.map((u) => u.name));
302
304
  manifest.units = [...serverlessUnits, mergedServerUnit];
305
+ // Anything that named one of the units just folded away has to follow it,
306
+ // or the manifest ships a queue consumer and a cron pointing at a unit
307
+ // that no longer exists.
308
+ for (const queue of manifest.queues) {
309
+ if (mergedNames.has(queue.consumerUnit)) {
310
+ queue.consumerUnit = serverUnitName;
311
+ }
312
+ }
313
+ for (const task of manifest.scheduledTasks) {
314
+ if (mergedNames.has(task.unitName)) {
315
+ task.unitName = serverUnitName;
316
+ }
317
+ }
318
+ for (const unit of manifest.units) {
319
+ unit.dependsOn = [
320
+ ...new Set(unit.dependsOn.map((dep) => mergedNames.has(dep) ? serverUnitName : dep)),
321
+ ].filter((dep) => dep !== unit.name);
322
+ }
303
323
  logger.info(` Server container: ${serverUnits.length} functions merged into one unit`);
304
324
  }
305
325
  logger.info(`Codegen complete: ${unitPikkuDirs.size} units`);
@@ -217,6 +217,7 @@ export const deployApply = pikkuSessionlessFunc({
217
217
  inspectorState,
218
218
  serverlessIncompatible: config.deploy?.serverlessIncompatible,
219
219
  defaultTarget: config.deploy?.defaultTarget,
220
+ grouping: config.deploy?.grouping,
220
221
  globalHTTPPrefix: config.globalHTTPPrefix,
221
222
  getEntryContext,
222
223
  frontend: config.frontend,
@@ -57,6 +57,7 @@ export const deployPlan = pikkuSessionlessFunc({
57
57
  inspectorState,
58
58
  serverlessIncompatible: config.deploy?.serverlessIncompatible,
59
59
  defaultTarget: config.deploy?.defaultTarget,
60
+ grouping: config.deploy?.grouping,
60
61
  globalHTTPPrefix: config.globalHTTPPrefix,
61
62
  getEntryContext,
62
63
  frontend: config.frontend,