@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.
- package/CHANGELOG.md +21 -0
- package/dist/.pikku/addon/index.d.ts +1 -1
- package/dist/.pikku/addon/index.js +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.js +1 -1
- package/dist/.pikku/agent/index.d.ts +1 -1
- package/dist/.pikku/agent/index.js +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/agent/pikku-model-aliases.gen.js +1 -1
- package/dist/.pikku/auth/index.d.ts +1 -1
- package/dist/.pikku/auth/index.js +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.d.ts +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.js +1 -1
- package/dist/.pikku/channel/index.d.ts +1 -1
- package/dist/.pikku/channel/index.js +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/index.d.ts +1 -1
- package/dist/.pikku/cli/index.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/error/index.d.ts +1 -1
- package/dist/.pikku/error/index.js +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.d.ts +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.js +1 -1
- package/dist/.pikku/function/index.d.ts +1 -1
- package/dist/.pikku/function/index.js +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +2 -2
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/gateway/index.d.ts +1 -1
- package/dist/.pikku/gateway/index.js +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.d.ts +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.js +1 -1
- package/dist/.pikku/http/index.d.ts +1 -1
- package/dist/.pikku/http/index.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/mcp/index.d.ts +1 -1
- package/dist/.pikku/mcp/index.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/middleware/index.d.ts +1 -1
- package/dist/.pikku/middleware/index.js +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.d.ts +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/queue/index.d.ts +1 -1
- package/dist/.pikku/queue/index.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/scenarios/index.d.ts +1 -1
- package/dist/.pikku/scenarios/index.js +1 -1
- package/dist/.pikku/scenarios/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-types.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.js +1 -1
- package/dist/.pikku/scheduler/index.d.ts +1 -1
- package/dist/.pikku/scheduler/index.js +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +1 -1
- package/dist/.pikku/scopes/index.d.ts +1 -1
- package/dist/.pikku/scopes/index.js +1 -1
- package/dist/.pikku/scopes/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-roles.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/secrets/index.d.ts +1 -1
- package/dist/.pikku/secrets/index.js +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/services/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/services/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/setup/index.d.ts +1 -1
- package/dist/.pikku/setup/index.js +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.d.ts +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.js +1 -1
- package/dist/.pikku/trigger/index.d.ts +1 -1
- package/dist/.pikku/trigger/index.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/index.d.ts +1 -1
- package/dist/.pikku/variables/index.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/index.d.ts +1 -1
- package/dist/.pikku/workflow/index.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/deploy/analyzer/analyzer.d.ts +8 -1
- package/dist/src/deploy/analyzer/analyzer.js +173 -51
- package/dist/src/deploy/analyzer/grouping.d.ts +23 -0
- package/dist/src/deploy/analyzer/grouping.js +74 -0
- package/dist/src/deploy/analyzer/index.d.ts +1 -0
- package/dist/src/deploy/analyzer/naming.d.ts +1 -0
- package/dist/src/deploy/analyzer/naming.js +9 -0
- package/dist/src/deploy/build-pipeline.d.ts +2 -0
- package/dist/src/deploy/build-pipeline.js +20 -0
- package/dist/src/functions/commands/deploy-apply.js +1 -0
- package/dist/src/functions/commands/deploy-plan.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- 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:
|
|
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:
|
|
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
|
-
|
|
132
|
-
name:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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,
|
|
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) =>
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
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,
|
|
887
|
+
function collectTags(funcIds, tagsFor) {
|
|
816
888
|
const tags = new Set();
|
|
817
889
|
for (const id of funcIds) {
|
|
818
|
-
const
|
|
819
|
-
|
|
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
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function toSafeKebab(str: string): string;
|
|
@@ -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,
|