@pikku/cli 0.12.93 → 0.12.96
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/console-app/assets/{index-CmAplHjZ.js → index-CeQodYfg.js} +149 -149
- package/console-app/assets/{index-BYj7ZbUU.css → index-CyYHTGWZ.css} +1 -1
- package/console-app/index.html +2 -2
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +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/pikku-cli-channel.d.ts +13 -15
- package/dist/.pikku/cli/pikku-cli-channel.js +62 -3
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +14 -4
- package/dist/.pikku/cli/pikku-cli-client.gen.js +20 -10
- 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-meta.gen.json +129 -2
- 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/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +55 -20
- package/dist/.pikku/function/pikku-function-types.gen.js +27 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +138 -13
- package/dist/.pikku/function/pikku-functions.gen.js +11 -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/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-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-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.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/rpc/pikku-rpc-wirings-meta.internal.gen.json +7 -0
- 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-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/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 +13 -3
- package/dist/.pikku/schemas/schemas/PersonaRunInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PersonaSyncInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuPersonasInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuRolesInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/RolesAuditInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/RolesPruneInput.schema.json +1 -0
- package/dist/.pikku/scopes/pikku-personas-meta.gen.json +1 -0
- package/dist/.pikku/scopes/pikku-personas.gen.d.ts +36 -0
- package/dist/.pikku/scopes/pikku-personas.gen.js +17 -0
- package/dist/.pikku/scopes/pikku-roles-meta.gen.json +1 -0
- package/dist/.pikku/scopes/pikku-roles.gen.d.ts +30 -0
- package/dist/.pikku/scopes/pikku-roles.gen.js +18 -0
- package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +4 -2
- package/dist/.pikku/scopes/pikku-scope-types.gen.js +3 -2
- package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +2 -2
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +2 -2
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.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/pikku-variable-types.gen.d.ts +2 -2
- package/dist/.pikku/variables/pikku-variable-types.gen.js +2 -2
- 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/meta/allWorkflow.gen.json +27 -3
- package/dist/.pikku/workflow/pikku-personas.gen.d.ts +15 -0
- package/dist/.pikku/workflow/pikku-personas.gen.js +17 -0
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +96 -19
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +22 -2
- 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/bin/pikku.js +0 -0
- package/dist/src/cli.wiring.js +101 -2
- package/dist/src/deploy/analyzer/analyzer.js +14 -0
- package/dist/src/deploy/analyzer/manifest.d.ts +11 -0
- package/dist/src/fabric/functions/validate.function.js +4 -4
- package/dist/src/functions/commands/bootstrap.js +3 -0
- package/dist/src/functions/commands/dev-ai-runner.d.ts +16 -4
- package/dist/src/functions/commands/dev-ai-runner.js +63 -29
- package/dist/src/functions/commands/dev.js +9 -4
- package/dist/src/functions/commands/enable.js +15 -19
- package/dist/src/functions/commands/{scenario-environment.d.ts → environment.d.ts} +9 -6
- package/dist/src/functions/commands/{scenario-environment.js → environment.js} +5 -4
- package/dist/src/functions/commands/new-addon.js +12 -12
- package/dist/src/functions/commands/persona-sync.d.ts +33 -0
- package/dist/src/functions/commands/persona-sync.js +151 -0
- package/dist/src/functions/commands/persona.d.ts +58 -0
- package/dist/src/functions/commands/persona.js +217 -0
- package/dist/src/functions/commands/roles-audit.d.ts +1 -0
- package/dist/src/functions/commands/roles-audit.js +37 -0
- package/dist/src/functions/commands/roles-prune.d.ts +16 -0
- package/dist/src/functions/commands/roles-prune.js +52 -0
- package/dist/src/functions/commands/roles-shared.d.ts +40 -0
- package/dist/src/functions/commands/roles-shared.js +77 -0
- package/dist/src/functions/commands/scenario-browser.d.ts +3 -3
- package/dist/src/functions/commands/scenario-browser.js +1 -1
- package/dist/src/functions/commands/scenario-ladder.d.ts +44 -2
- package/dist/src/functions/commands/scenario-ladder.js +51 -2
- package/dist/src/functions/commands/scenario.d.ts +7 -0
- package/dist/src/functions/commands/scenario.js +79 -17
- package/dist/src/functions/commands/scopes-shared.d.ts +8 -0
- package/dist/src/functions/commands/scopes-shared.js +1 -1
- package/dist/src/functions/commands/serve.js +7 -3
- package/dist/src/functions/commands/virtual-user-formatter.d.ts +30 -0
- package/dist/src/functions/commands/virtual-user-formatter.js +65 -0
- package/dist/src/functions/db/local-db.js +38 -1
- package/dist/src/functions/validate/workspace-validate.js +1 -1
- package/dist/src/functions/wirings/ai-agent/pikku-command-public-agent.js +2 -1
- package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +1 -0
- package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +2 -2
- package/dist/src/functions/wirings/auth/serialize-auth-gen.js +8 -8
- package/dist/src/functions/wirings/cli/serialize-channel-cli-client.d.ts +7 -3
- package/dist/src/functions/wirings/cli/serialize-channel-cli-client.js +32 -18
- package/dist/src/functions/wirings/cli/serialize-channel-cli.js +63 -6
- package/dist/src/functions/wirings/console/serialize-console-functions.js +4 -7
- package/dist/src/functions/wirings/functions/serialize-function-types.js +88 -28
- package/dist/src/functions/wirings/middleware/serialize-middleware-imports.js +25 -9
- package/dist/src/functions/wirings/package/serialize-package-types.js +5 -3
- package/dist/src/functions/wirings/personas/pikku-command-personas.d.ts +7 -0
- package/dist/src/functions/wirings/personas/pikku-command-personas.js +43 -0
- package/dist/src/functions/wirings/personas/serialize-personas-types.d.ts +18 -0
- package/dist/src/functions/wirings/personas/serialize-personas-types.js +76 -0
- package/dist/src/functions/wirings/realtime/pikku-command-events-scaffold.js +2 -1
- package/dist/src/functions/wirings/roles/pikku-command-roles.d.ts +7 -0
- package/dist/src/functions/wirings/roles/pikku-command-roles.js +33 -0
- package/dist/src/functions/wirings/roles/serialize-roles-types.d.ts +14 -0
- package/dist/src/functions/wirings/roles/serialize-roles-types.js +55 -0
- package/dist/src/functions/wirings/rpc/pikku-command-public-rpc.js +2 -1
- package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +3 -3
- package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +3 -3
- package/dist/src/functions/wirings/scopes/serialize-scopes-types.d.ts +1 -1
- package/dist/src/functions/wirings/scopes/serialize-scopes-types.js +1 -1
- package/dist/src/functions/wirings/user-admin/pikku-command-user-admin-functions.js +2 -1
- package/dist/src/functions/wirings/user-admin/serialize-user-admin-functions.js +2 -2
- package/dist/src/functions/wirings/workflow/pikku-command-workflow-routes.js +2 -1
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +13 -13
- package/dist/src/functions/wirings/workflow/serialize-personas.d.ts +10 -0
- package/dist/src/functions/wirings/workflow/serialize-personas.js +46 -0
- package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +11 -2
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +165 -29
- package/dist/src/functions/workflows/all.workflow.js +7 -1
- package/dist/src/server/content-signing-jwt.d.ts +13 -0
- package/dist/src/server/content-signing-jwt.js +51 -0
- package/dist/src/server/dev-server-runner.interface.d.ts +10 -2
- package/dist/src/server/node-server-runner.d.ts +2 -2
- package/dist/src/server/node-server-runner.js +2 -1
- package/dist/src/services.d.ts +3 -1
- package/dist/src/services.js +2 -1
- package/dist/src/utils/pikku-cli-config.js +45 -2
- package/dist/src/utils/resolve-personas.d.ts +32 -0
- package/dist/src/utils/resolve-personas.js +46 -0
- package/dist/src/utils/resolve-scaffold-feature.d.ts +19 -0
- package/dist/src/utils/resolve-scaffold-feature.js +49 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -14
- package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +0 -1
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +0 -19
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +0 -17
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +0 -2
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +0 -40
- package/dist/src/utils/resolve-scenario-actors.d.ts +0 -23
- package/dist/src/utils/resolve-scenario-actors.js +0 -74
|
@@ -7,10 +7,22 @@ import type { AIAgentRunnerService } from '@pikku/core/services';
|
|
|
7
7
|
* has no equivalent, so agents 503 with AIProviderNotConfiguredError unless we
|
|
8
8
|
* construct one here. When an OpenAI-compatible base URL + key are present
|
|
9
9
|
* (fabric injects LITELLM_PROXY_URL/LITELLM_API_KEY; the standard OPENAI_*
|
|
10
|
-
* vars are also honored) we point
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* vars are also honored) we point one provider at it and register it under
|
|
11
|
+
* `'*'`, so every provider prefix resolves to it. The CLI ships the SDK rather
|
|
12
|
+
* than asking the project for it — behind a proxy one openai-compatible
|
|
13
|
+
* provider answers for `openai/…`, `anthropic/…` and `google/…` alike, so there
|
|
14
|
+
* was never a per-vendor package worth making somebody install. The project's
|
|
15
|
+
* own copies still win when it has them. Returns undefined when no AI env is
|
|
16
|
+
* configured (agents stay disabled, with the clear downstream error) or when
|
|
17
|
+
* the SDK packages cannot be loaded from either place.
|
|
18
|
+
*
|
|
19
|
+
* Audio does not work here: `@ai-sdk/openai-compatible` exposes only
|
|
20
|
+
* language/embedding/image models, so a voice agent under `pikku dev` throws
|
|
21
|
+
* `Provider does not support transcription models`. Fixing that means the full
|
|
22
|
+
* `@ai-sdk/openai` provider, which assumes OpenAI's own request specifics — a
|
|
23
|
+
* bad default for a path whose entire purpose is fronting arbitrary gateways.
|
|
24
|
+
* If it becomes worth it, delegate just `transcription`/`speech` to the full
|
|
25
|
+
* provider and leave everything else on this one.
|
|
14
26
|
*/
|
|
15
27
|
export declare function createDevAIAgentRunner({ logger, projectRoot, variables, }: {
|
|
16
28
|
logger: Logger;
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
|
-
// Provider prefixes a fabric/proxy baseURL fronts. Models are written as
|
|
5
|
-
// `openai/gpt-4o-mini`, `openai/deepseek-v4-flash`, etc. — the runner splits on
|
|
6
|
-
// the first `/`, so the prefix only selects the provider entry and the bare
|
|
7
|
-
// model name is forwarded to the OpenAI-compatible proxy, which routes it.
|
|
8
|
-
const PROXY_PROVIDER_NAMES = [
|
|
9
|
-
'openai',
|
|
10
|
-
'anthropic',
|
|
11
|
-
'google',
|
|
12
|
-
'gemini',
|
|
13
|
-
'deepseek',
|
|
14
|
-
'xai',
|
|
15
|
-
'litellm',
|
|
16
|
-
];
|
|
17
4
|
/**
|
|
18
5
|
* Build the AI agent runner for `pikku dev` from env.
|
|
19
6
|
*
|
|
@@ -21,10 +8,22 @@ const PROXY_PROVIDER_NAMES = [
|
|
|
21
8
|
* has no equivalent, so agents 503 with AIProviderNotConfiguredError unless we
|
|
22
9
|
* construct one here. When an OpenAI-compatible base URL + key are present
|
|
23
10
|
* (fabric injects LITELLM_PROXY_URL/LITELLM_API_KEY; the standard OPENAI_*
|
|
24
|
-
* vars are also honored) we point
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
11
|
+
* vars are also honored) we point one provider at it and register it under
|
|
12
|
+
* `'*'`, so every provider prefix resolves to it. The CLI ships the SDK rather
|
|
13
|
+
* than asking the project for it — behind a proxy one openai-compatible
|
|
14
|
+
* provider answers for `openai/…`, `anthropic/…` and `google/…` alike, so there
|
|
15
|
+
* was never a per-vendor package worth making somebody install. The project's
|
|
16
|
+
* own copies still win when it has them. Returns undefined when no AI env is
|
|
17
|
+
* configured (agents stay disabled, with the clear downstream error) or when
|
|
18
|
+
* the SDK packages cannot be loaded from either place.
|
|
19
|
+
*
|
|
20
|
+
* Audio does not work here: `@ai-sdk/openai-compatible` exposes only
|
|
21
|
+
* language/embedding/image models, so a voice agent under `pikku dev` throws
|
|
22
|
+
* `Provider does not support transcription models`. Fixing that means the full
|
|
23
|
+
* `@ai-sdk/openai` provider, which assumes OpenAI's own request specifics — a
|
|
24
|
+
* bad default for a path whose entire purpose is fronting arbitrary gateways.
|
|
25
|
+
* If it becomes worth it, delegate just `transcription`/`speech` to the full
|
|
26
|
+
* provider and leave everything else on this one.
|
|
28
27
|
*/
|
|
29
28
|
export async function createDevAIAgentRunner({ logger, projectRoot, variables, }) {
|
|
30
29
|
// Pair the URL with its matching key — coalescing each var independently could
|
|
@@ -43,28 +42,63 @@ export async function createDevAIAgentRunner({ logger, projectRoot, variables, }
|
|
|
43
42
|
logger.debug('pikku dev: no AI provider env (OPENAI_BASE_URL/OPENAI_API_KEY or LITELLM_PROXY_URL/LITELLM_API_KEY) — AI agents disabled');
|
|
44
43
|
return undefined;
|
|
45
44
|
}
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
45
|
+
// The project's own copies win, so an app that pins an `ai` version gets a
|
|
46
|
+
// runner built from that one rather than a second copy alongside it. The
|
|
47
|
+
// CLI's are the fallback: this function exists for projects that have not
|
|
48
|
+
// installed an AI SDK at all, and behind a proxy a single openai-compatible
|
|
49
|
+
// provider reaches every model, so making someone add three packages before
|
|
50
|
+
// agents work in dev buys nothing.
|
|
51
|
+
const fromProject = createRequire(pathToFileURL(join(projectRoot, 'package.json')).href);
|
|
52
|
+
//
|
|
53
|
+
// Both come from the same place or neither does. A provider built against one
|
|
54
|
+
// copy of `@ai-sdk/provider` handed to a runner built against another is the
|
|
55
|
+
// one combination that fails at call time rather than at load time, with an
|
|
56
|
+
// error about the model spec that names neither package.
|
|
57
|
+
const loadPair = async () => {
|
|
58
|
+
const project = ['@pikku/ai-vercel', '@ai-sdk/openai-compatible'].map((name) => {
|
|
59
|
+
try {
|
|
60
|
+
return pathToFileURL(fromProject.resolve(name)).href;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const [runner, provider] = project.every(Boolean)
|
|
67
|
+
? project
|
|
68
|
+
: ['@pikku/ai-vercel', '@ai-sdk/openai-compatible'];
|
|
69
|
+
return Promise.all([import(runner), import(provider)]);
|
|
70
|
+
};
|
|
49
71
|
let VercelAIAgentRunner;
|
|
50
72
|
let createOpenAICompatible;
|
|
51
73
|
try {
|
|
52
|
-
;
|
|
53
|
-
({ VercelAIAgentRunner } =
|
|
54
|
-
({ createOpenAICompatible } =
|
|
74
|
+
const [runnerModule, providerModule] = await loadPair();
|
|
75
|
+
({ VercelAIAgentRunner } = runnerModule);
|
|
76
|
+
({ createOpenAICompatible } = providerModule);
|
|
55
77
|
}
|
|
56
78
|
catch (error) {
|
|
57
|
-
logger.warn(`pikku dev: AI provider env is set but the AI SDK packages could not be loaded
|
|
79
|
+
logger.warn(`pikku dev: AI provider env is set but the AI SDK packages could not be loaded — AI agents disabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
80
|
return undefined;
|
|
59
81
|
}
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
// One provider under '*', so every prefix a model might name resolves to the
|
|
83
|
+
// proxy — including ones nobody thought to list. This was a hardcoded array
|
|
84
|
+
// of seven names, which silently excluded everything else.
|
|
85
|
+
//
|
|
86
|
+
// `supportsStructuredOutputs` defaults to false, and the default fails in the
|
|
87
|
+
// worst available way: an `outputSchema` is dropped, the call degrades to
|
|
88
|
+
// bare `json_object`, and the model returns well-formed JSON with whatever
|
|
89
|
+
// keys it felt like. Nothing errors — the caller reads `undefined` off every
|
|
90
|
+
// field it asked for and behaves as though the model said nothing. Anything
|
|
91
|
+
// fronting this URL that is worth calling speaks `json_schema`; the ones that
|
|
92
|
+
// do not now fail loudly on the first schema'd call, which is the outcome we
|
|
93
|
+
// want over silently ignoring the schema.
|
|
94
|
+
const buildProviders = (key) => ({
|
|
95
|
+
'*': createOpenAICompatible({
|
|
62
96
|
name: 'pikku-dev',
|
|
63
97
|
baseURL,
|
|
64
98
|
apiKey: key,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
99
|
+
supportsStructuredOutputs: true,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
68
102
|
logger.info(`pikku dev: AI agent runner wired to ${baseURL}`);
|
|
69
103
|
return new VercelAIAgentRunner(buildProviders(apiKey), buildProviders);
|
|
70
104
|
}
|
|
@@ -16,6 +16,8 @@ import { startCoverageService } from './start-coverage.js';
|
|
|
16
16
|
import { createDevAIAgentRunner } from './dev-ai-runner.js';
|
|
17
17
|
import { resolveConsoleMount } from './serve-console.js';
|
|
18
18
|
import { serverReadyLine } from '../../server/server-ready.js';
|
|
19
|
+
import { createEphemeralContentSigningJWT } from '../../server/content-signing-jwt.js';
|
|
20
|
+
import { resolveScaffoldFeature } from '../../utils/resolve-scaffold-feature.js';
|
|
19
21
|
export const dev = pikkuSessionlessFunc({
|
|
20
22
|
remote: true,
|
|
21
23
|
func: async ({ logger, config, getInspectorState, invalidateInspectorState, variables, devServerRunner, }, { port, watch, hmr, coverage, test }, { rpc }) => {
|
|
@@ -122,7 +124,7 @@ export const dev = pikkuSessionlessFunc({
|
|
|
122
124
|
// project — an app bootstrap is what a deployed bundle imports, and coverage
|
|
123
125
|
// endpoints have no business in one.
|
|
124
126
|
if (config.scaffold?.scenarios) {
|
|
125
|
-
registerScenarioInstrumentation(config.scaffold.scenarios
|
|
127
|
+
registerScenarioInstrumentation(resolveScaffoldFeature('scenarios', config.scaffold.scenarios).auth);
|
|
126
128
|
}
|
|
127
129
|
const configModule = await loadUserModule(pikkuConfigFactory.file);
|
|
128
130
|
const servicesModule = await loadUserModule(singletonServicesFactory.file);
|
|
@@ -150,8 +152,11 @@ export const dev = pikkuSessionlessFunc({
|
|
|
150
152
|
sizeLimit: userConfig.content.sizeLimit,
|
|
151
153
|
}
|
|
152
154
|
: undefined;
|
|
153
|
-
const
|
|
154
|
-
?
|
|
155
|
+
const contentSigningJWT = localContentConfig
|
|
156
|
+
? createEphemeralContentSigningJWT()
|
|
157
|
+
: undefined;
|
|
158
|
+
const localContent = localContentConfig && contentSigningJWT
|
|
159
|
+
? new LocalContent(localContentConfig, logger, contentSigningJWT)
|
|
155
160
|
: undefined;
|
|
156
161
|
const schedulerService = new InMemorySchedulerService();
|
|
157
162
|
const aiStorage = kysely
|
|
@@ -239,7 +244,7 @@ export const dev = pikkuSessionlessFunc({
|
|
|
239
244
|
port: resolvedPort,
|
|
240
245
|
content: localContentConfig,
|
|
241
246
|
...(consoleMount ? { staticMounts: [consoleMount] } : {}),
|
|
242
|
-
}, logger);
|
|
247
|
+
}, logger, { contentSigningJWT });
|
|
243
248
|
const lifecycle = await loadLifecycle();
|
|
244
249
|
await pikkuServer.init();
|
|
245
250
|
await lifecycle?.beforeStart?.(resolvedServices);
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { pikkuVoidFunc } from '#pikku';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
events: 'auth',
|
|
11
|
-
remoteRpc: 'no-auth',
|
|
12
|
-
};
|
|
4
|
+
/**
|
|
5
|
+
* Remote RPC workers carry no auth dimension — `serialize-remote-rpc.ts`
|
|
6
|
+
* hardcodes `auth: false` on what it generates — so enabling it writes a bare
|
|
7
|
+
* `true` and there is nothing to opt out of.
|
|
8
|
+
*/
|
|
9
|
+
const NO_AUTH_DIMENSION = new Set(['remoteRpc']);
|
|
13
10
|
async function enableFeature(feature, logger, config, data) {
|
|
14
11
|
const noAuth = data?.noAuth ?? false;
|
|
15
12
|
const configPath = join(config.configDir, 'pikku.config.json');
|
|
@@ -21,18 +18,17 @@ async function enableFeature(feature, logger, config, data) {
|
|
|
21
18
|
if (!json.scaffold.pikkuDir) {
|
|
22
19
|
json.scaffold.pikkuDir = 'pikku';
|
|
23
20
|
}
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
? 'no-auth'
|
|
32
|
-
: FEATURE_DEFAULTS[feature];
|
|
21
|
+
// `true` is enabled AND authenticated. Only an explicit --no-auth writes the
|
|
22
|
+
// object form, so a feature can never be turned on public by accident.
|
|
23
|
+
//
|
|
24
|
+
// The console is an admin surface — every RPC requires a session, so
|
|
25
|
+
// --no-auth is ignored for it.
|
|
26
|
+
const wantsPublic = noAuth && feature !== 'console' && !NO_AUTH_DIMENSION.has(feature);
|
|
27
|
+
const value = feature === 'webhook' ? true : wantsPublic ? { auth: false } : true;
|
|
33
28
|
json.scaffold[feature] = value;
|
|
34
29
|
await writeFile(configPath, JSON.stringify(json, null, 2) + '\n', 'utf-8');
|
|
35
|
-
logger.info(`Enabled scaffold.${feature} =
|
|
30
|
+
logger.info(`Enabled scaffold.${feature} = ${JSON.stringify(value)} in ${configPath}` +
|
|
31
|
+
(wantsPublic ? ' (public — no session required)' : ''));
|
|
36
32
|
}
|
|
37
33
|
export const enableRpc = pikkuVoidFunc({
|
|
38
34
|
func: async ({ logger, config }, data) => enableFeature('rpc', logger, config, data),
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolving `pikku scenario run <environment>`
|
|
2
|
+
* Resolving `pikku scenario run <environment>` and `pikku persona run
|
|
3
|
+
* <environment>` to the URLs the run targets.
|
|
3
4
|
*
|
|
4
5
|
* The configured environment is the base; `--api-url`/`--app-url` override it
|
|
5
6
|
* for one invocation, which is what makes a suite runnable against a target
|
|
6
7
|
* that only exists at run time (a freshly provisioned sandbox) without writing
|
|
7
8
|
* a pikku.config.json per deployment.
|
|
8
9
|
*/
|
|
9
|
-
/** One entry of `
|
|
10
|
-
export interface
|
|
10
|
+
/** One entry of `environments` in pikku.config.json. */
|
|
11
|
+
export interface PikkuEnvironment {
|
|
11
12
|
apiUrl: string;
|
|
12
13
|
signInPath?: string;
|
|
13
14
|
rpcPath?: string;
|
|
14
15
|
appUrl?: string;
|
|
16
|
+
/** Real consequences: only an `accountable` persona may run against it. */
|
|
17
|
+
production?: boolean;
|
|
15
18
|
}
|
|
16
|
-
export interface
|
|
19
|
+
export interface ResolveEnvironmentOptions {
|
|
17
20
|
/** The name given on the command line. Must exist in config. */
|
|
18
21
|
environment: string;
|
|
19
|
-
environments: Record<string,
|
|
22
|
+
environments: Record<string, PikkuEnvironment>;
|
|
20
23
|
/** `--api-url`, overriding the configured `apiUrl`. */
|
|
21
24
|
apiUrl?: string;
|
|
22
25
|
/** `--app-url`, overriding the configured `appUrl`. */
|
|
@@ -29,4 +32,4 @@ export interface ResolveScenarioEnvironmentOptions {
|
|
|
29
32
|
* all see the same target. The environment is still looked up by name: the
|
|
30
33
|
* flags override fields on a configured environment, they do not invent one.
|
|
31
34
|
*/
|
|
32
|
-
export declare const
|
|
35
|
+
export declare const resolveEnvironment: ({ environment, environments, apiUrl, appUrl, spawn, }: ResolveEnvironmentOptions) => PikkuEnvironment;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolving `pikku scenario run <environment>`
|
|
2
|
+
* Resolving `pikku scenario run <environment>` and `pikku persona run
|
|
3
|
+
* <environment>` to the URLs the run targets.
|
|
3
4
|
*
|
|
4
5
|
* The configured environment is the base; `--api-url`/`--app-url` override it
|
|
5
6
|
* for one invocation, which is what makes a suite runnable against a target
|
|
@@ -35,14 +36,14 @@ const assertAbsoluteUrl = (flag, value) => {
|
|
|
35
36
|
* all see the same target. The environment is still looked up by name: the
|
|
36
37
|
* flags override fields on a configured environment, they do not invent one.
|
|
37
38
|
*/
|
|
38
|
-
export const
|
|
39
|
+
export const resolveEnvironment = ({ environment, environments, apiUrl, appUrl, spawn = false, }) => {
|
|
39
40
|
const configured = environments[environment];
|
|
40
41
|
if (!configured) {
|
|
41
42
|
const known = Object.keys(environments);
|
|
42
|
-
throw new Error(`Unknown
|
|
43
|
+
throw new Error(`Unknown environment '${environment}'. ` +
|
|
43
44
|
(known.length
|
|
44
45
|
? `Configured environments: ${known.join(', ')}`
|
|
45
|
-
: `Add
|
|
46
|
+
: `Add environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
|
|
46
47
|
}
|
|
47
48
|
const resolved = { ...configured };
|
|
48
49
|
if (apiUrl !== undefined) {
|
|
@@ -566,13 +566,13 @@ export interface Services extends CoreServices<SingletonServices> {}
|
|
|
566
566
|
// Conditional: credential / secret file
|
|
567
567
|
if (flags.credential === 'apikey') {
|
|
568
568
|
files[`src/${name}.credential.ts`] = `import { z } from 'zod'
|
|
569
|
-
import {
|
|
569
|
+
import { defineCredential } from '@pikku/core/credential'
|
|
570
570
|
|
|
571
571
|
export const ${camelName}CredentialSchema = z.object({
|
|
572
572
|
apiKey: z.string().describe(${literal(`${displayName} API key`)}),
|
|
573
573
|
})
|
|
574
574
|
|
|
575
|
-
|
|
575
|
+
defineCredential({
|
|
576
576
|
name: '${camelName}',
|
|
577
577
|
displayName: ${literal(displayName)},
|
|
578
578
|
description: ${literal(description)},
|
|
@@ -588,13 +588,13 @@ wireCredential({
|
|
|
588
588
|
tenantId: z.string().optional().describe('Upstream tenant id'),`
|
|
589
589
|
: ` token: z.string().describe(${literal(`${displayName} bearer token`)}),`;
|
|
590
590
|
files[`src/${name}.credential.ts`] = `import { z } from 'zod'
|
|
591
|
-
import {
|
|
591
|
+
import { defineCredential } from '@pikku/core/credential'
|
|
592
592
|
|
|
593
593
|
export const ${camelName}CredentialSchema = z.object({
|
|
594
594
|
${credentialFields}
|
|
595
595
|
})
|
|
596
596
|
|
|
597
|
-
|
|
597
|
+
defineCredential({
|
|
598
598
|
name: '${camelName}',
|
|
599
599
|
displayName: ${literal(displayName)},
|
|
600
600
|
description: ${literal(description)},
|
|
@@ -605,15 +605,15 @@ wireCredential({
|
|
|
605
605
|
}
|
|
606
606
|
else if (flags.oauth || flags.credential === 'oauth2') {
|
|
607
607
|
files[`src/${name}.credential.ts`] = `import { z } from 'zod'
|
|
608
|
-
import {
|
|
609
|
-
import {
|
|
608
|
+
import { defineCredential } from '@pikku/core/credential'
|
|
609
|
+
import { defineSecret } from '@pikku/core/secret'
|
|
610
610
|
|
|
611
611
|
export const ${camelName}TokenSchema = z.object({
|
|
612
612
|
accessToken: z.string(),
|
|
613
613
|
refreshToken: z.string().optional(),
|
|
614
614
|
})
|
|
615
615
|
|
|
616
|
-
|
|
616
|
+
defineCredential({
|
|
617
617
|
name: '${camelName}',
|
|
618
618
|
displayName: ${literal(displayName)},
|
|
619
619
|
description: ${literal(description)},
|
|
@@ -628,7 +628,7 @@ wireCredential({
|
|
|
628
628
|
},
|
|
629
629
|
})
|
|
630
630
|
|
|
631
|
-
|
|
631
|
+
defineSecret({
|
|
632
632
|
name: '${camelName}OAuthApp',
|
|
633
633
|
displayName: ${literal(`${displayName} OAuth App`)},
|
|
634
634
|
description: ${literal(`OAuth2 app credentials for ${displayName}`)},
|
|
@@ -638,7 +638,7 @@ wireSecret({
|
|
|
638
638
|
}
|
|
639
639
|
else if (flags.secret) {
|
|
640
640
|
files[`src/${name}.secret.ts`] = `import { z } from 'zod'
|
|
641
|
-
import {
|
|
641
|
+
import { defineSecret } from '@pikku/core/secret'
|
|
642
642
|
|
|
643
643
|
export const ${camelName}SecretsSchema = z.object({
|
|
644
644
|
apiKey: z.string().describe(${literal(`${displayName} API key`)}),
|
|
@@ -647,7 +647,7 @@ export const ${camelName}SecretsSchema = z.object({
|
|
|
647
647
|
|
|
648
648
|
export type ${pascalName}Secrets = z.infer<typeof ${camelName}SecretsSchema>
|
|
649
649
|
|
|
650
|
-
|
|
650
|
+
defineSecret({
|
|
651
651
|
name: '${camelName}',
|
|
652
652
|
displayName: ${literal(`${displayName} API`)},
|
|
653
653
|
description: ${literal(description)},
|
|
@@ -659,11 +659,11 @@ wireSecret({
|
|
|
659
659
|
// Conditional: variable file
|
|
660
660
|
if (flags.variable) {
|
|
661
661
|
files[`src/${name}.variable.ts`] = `import { z } from 'zod'
|
|
662
|
-
import {
|
|
662
|
+
import { defineVariable } from '@pikku/core/variable'
|
|
663
663
|
|
|
664
664
|
export const ${camelName}VariableSchema = z.string().optional().describe('TODO: describe this variable')
|
|
665
665
|
|
|
666
|
-
|
|
666
|
+
defineVariable({
|
|
667
667
|
name: '${camelName}_variable',
|
|
668
668
|
displayName: ${literal(`${displayName} Variable`)},
|
|
669
669
|
description: 'TODO: describe this variable',
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provision the declared personas into an environment: their accounts, and the
|
|
3
|
+
* roles they declare.
|
|
4
|
+
*
|
|
5
|
+
* This is deployment, not seeding. `pikku db seed` applies a project's test
|
|
6
|
+
* fixtures and does not run in production; an accountable persona doing a real
|
|
7
|
+
* job in production still needs an account and its grants, and this is where
|
|
8
|
+
* they come from.
|
|
9
|
+
*
|
|
10
|
+
* Additive in the same way `syncSystemRoles` is: it creates and grants, and
|
|
11
|
+
* never deletes or revokes. Removing a persona from the code leaves their
|
|
12
|
+
* account and their grants standing until somebody decides otherwise — which is
|
|
13
|
+
* the point, because the alternative is a rolling deploy quietly locking out the
|
|
14
|
+
* teammate that the older replica is still running.
|
|
15
|
+
*
|
|
16
|
+
* It needs both halves of an environment: its API, to sign the persona in, and
|
|
17
|
+
* its database, to write the grants. They must be the same deployment — the
|
|
18
|
+
* command has no way to check that, so pointing it at a stage's API and a local
|
|
19
|
+
* database provisions nobody usefully.
|
|
20
|
+
*/
|
|
21
|
+
export declare const personaSync: import("#pikku").PikkuFunctionConfig<{
|
|
22
|
+
environment: string;
|
|
23
|
+
apiUrl?: string;
|
|
24
|
+
dryRun?: boolean;
|
|
25
|
+
}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
|
|
26
|
+
environment: string;
|
|
27
|
+
apiUrl?: string;
|
|
28
|
+
dryRun?: boolean;
|
|
29
|
+
}, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
|
|
30
|
+
environment: string;
|
|
31
|
+
apiUrl?: string;
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
}, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
+
import { createHttpPersonas } from '@pikku/core/persona';
|
|
3
|
+
import { personaEnvironmentRefusal } from '@pikku/core/persona';
|
|
4
|
+
import { resolvePersonas } from '../../utils/resolve-personas.js';
|
|
5
|
+
import { resolveEnvironment } from './environment.js';
|
|
6
|
+
import { loadDeclaredRoles, openScopeServiceForRoles } from './roles-shared.js';
|
|
7
|
+
/**
|
|
8
|
+
* The user id behind a persona's address.
|
|
9
|
+
*
|
|
10
|
+
* Read straight off better-auth's own `user` table rather than through the
|
|
11
|
+
* ScopeService, because the ScopeService has no notion of an address — its
|
|
12
|
+
* grants are keyed by user id, and `pikku_user_role.user_id` already FKs into
|
|
13
|
+
* this exact table. Looking it up anywhere else would mean inventing a second
|
|
14
|
+
* account concept for personas, which is the one thing this command exists to
|
|
15
|
+
* avoid: a persona is a user, provisioned the way users are.
|
|
16
|
+
*/
|
|
17
|
+
const userIdByEmail = async (db, email) => {
|
|
18
|
+
const row = await db
|
|
19
|
+
.selectFrom('user')
|
|
20
|
+
.select('id')
|
|
21
|
+
.where('email', '=', email)
|
|
22
|
+
.executeTakeFirst();
|
|
23
|
+
return row ? String(row.id) : null;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Provision the declared personas into an environment: their accounts, and the
|
|
27
|
+
* roles they declare.
|
|
28
|
+
*
|
|
29
|
+
* This is deployment, not seeding. `pikku db seed` applies a project's test
|
|
30
|
+
* fixtures and does not run in production; an accountable persona doing a real
|
|
31
|
+
* job in production still needs an account and its grants, and this is where
|
|
32
|
+
* they come from.
|
|
33
|
+
*
|
|
34
|
+
* Additive in the same way `syncSystemRoles` is: it creates and grants, and
|
|
35
|
+
* never deletes or revokes. Removing a persona from the code leaves their
|
|
36
|
+
* account and their grants standing until somebody decides otherwise — which is
|
|
37
|
+
* the point, because the alternative is a rolling deploy quietly locking out the
|
|
38
|
+
* teammate that the older replica is still running.
|
|
39
|
+
*
|
|
40
|
+
* It needs both halves of an environment: its API, to sign the persona in, and
|
|
41
|
+
* its database, to write the grants. They must be the same deployment — the
|
|
42
|
+
* command has no way to check that, so pointing it at a stage's API and a local
|
|
43
|
+
* database provisions nobody usefully.
|
|
44
|
+
*/
|
|
45
|
+
export const personaSync = pikkuSessionlessFunc({
|
|
46
|
+
remote: true,
|
|
47
|
+
func: async ({ logger, config, getInspectorState, variables }, { environment, apiUrl, dryRun }) => {
|
|
48
|
+
const state = await getInspectorState(true, false, false, true);
|
|
49
|
+
const personas = resolvePersonas(state.personas?.definitions ?? [], config.scenarios?.emailDomain);
|
|
50
|
+
if (Object.keys(personas).length === 0) {
|
|
51
|
+
logger.info('persona sync: no personas are declared — add a definePersonas({ … }) call.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const environments = config.environments ?? {};
|
|
55
|
+
const env = resolveEnvironment({ environment, environments, apiUrl });
|
|
56
|
+
// The same rule that decides who may *run* against an environment decides
|
|
57
|
+
// who may be provisioned into it. Two rules would drift, and the one that
|
|
58
|
+
// drifted would leave an account standing in production for a persona the
|
|
59
|
+
// engine then refuses to sign in.
|
|
60
|
+
//
|
|
61
|
+
// A `runnable: false` persona is still provisioned: their account is the
|
|
62
|
+
// whole reason they were declared, because other people act on it.
|
|
63
|
+
const eligible = [];
|
|
64
|
+
const skipped = [];
|
|
65
|
+
for (const [id, persona] of Object.entries(personas)) {
|
|
66
|
+
const refusal = personaEnvironmentRefusal(id, persona, environment, environments);
|
|
67
|
+
if (refusal) {
|
|
68
|
+
skipped.push(refusal);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
eligible.push([id, persona]);
|
|
72
|
+
}
|
|
73
|
+
if (eligible.length === 0) {
|
|
74
|
+
logger.info(`persona sync: no persona may act in '${environment}' — ${skipped.length} declared persona(s) were all skipped.`);
|
|
75
|
+
logger.info(' Run with --dry-run for the reasons, or give the persona this environment and disposition `accountable` if it is production.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (dryRun) {
|
|
79
|
+
logger.info(`persona sync (dry run): ${eligible.length} persona(s) would be provisioned into '${environment}' at ${env.apiUrl}:`);
|
|
80
|
+
for (const [id, persona] of eligible) {
|
|
81
|
+
logger.info(` ${id.padEnd(20)} ${persona.email}${persona.roles.length
|
|
82
|
+
? ` -> ${persona.roles.join(' + ')}`
|
|
83
|
+
: ' (no roles)'}`);
|
|
84
|
+
}
|
|
85
|
+
for (const refusal of skipped) {
|
|
86
|
+
logger.info(` skipped: ${refusal}`);
|
|
87
|
+
}
|
|
88
|
+
logger.info('Re-run without --dry-run to apply.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const secret = await variables.get('SCENARIO_ACTOR_SECRET');
|
|
92
|
+
if (!secret) {
|
|
93
|
+
throw new Error('SCENARIO_ACTOR_SECRET is not set — a persona account cannot be created. ' +
|
|
94
|
+
'Export it in the environment running this command (never put it in pikku.config.json).');
|
|
95
|
+
}
|
|
96
|
+
const declaredRoles = await loadDeclaredRoles(config.rolesMetaJsonFile, logger);
|
|
97
|
+
if (!declaredRoles) {
|
|
98
|
+
throw new Error('role metadata not found');
|
|
99
|
+
}
|
|
100
|
+
const signedIn = createHttpPersonas({
|
|
101
|
+
apiUrl: env.apiUrl,
|
|
102
|
+
secret,
|
|
103
|
+
personas: Object.fromEntries(eligible),
|
|
104
|
+
signInPath: env.signInPath,
|
|
105
|
+
rpcPath: env.rpcPath,
|
|
106
|
+
});
|
|
107
|
+
// Roles are synced on the way in, so a grant can name a role this deploy
|
|
108
|
+
// introduced. Both that sync and everything below are additive.
|
|
109
|
+
const opened = await openScopeServiceForRoles({ config, logger }, declaredRoles, 'pikku persona sync');
|
|
110
|
+
if (!opened) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
let accounts = 0;
|
|
115
|
+
let granted = 0;
|
|
116
|
+
let held = 0;
|
|
117
|
+
for (const [id, persona] of eligible) {
|
|
118
|
+
// Signing in is what creates the account: the actor plugin upserts an
|
|
119
|
+
// `actor: true` user row on first sign-in, and nothing else does.
|
|
120
|
+
// `sessionRoles()` is the public call that forces it — its answer is a
|
|
121
|
+
// by-product here, not the reason for the call.
|
|
122
|
+
await signedIn[id].sessionRoles();
|
|
123
|
+
accounts++;
|
|
124
|
+
if (persona.roles.length === 0) {
|
|
125
|
+
logger.info(` ${id.padEnd(20)} ${persona.email} (no roles)`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const userId = await userIdByEmail(opened.db, persona.email);
|
|
129
|
+
if (!userId) {
|
|
130
|
+
throw new Error(`persona sync: signed '${id}' in at ${env.apiUrl}, but no user row for ${persona.email} exists in the database this command opened. ` +
|
|
131
|
+
`That database is not the one '${environment}' runs on, so its grants would land nowhere.`);
|
|
132
|
+
}
|
|
133
|
+
const already = new Set(await opened.service.listUserRoles(userId));
|
|
134
|
+
const missing = persona.roles.filter((role) => !already.has(role));
|
|
135
|
+
for (const role of missing) {
|
|
136
|
+
await opened.service.addUserToRole(userId, role);
|
|
137
|
+
}
|
|
138
|
+
granted += missing.length;
|
|
139
|
+
held += persona.roles.length - missing.length;
|
|
140
|
+
logger.info(` ${id.padEnd(20)} ${persona.email} -> ${persona.roles.join(' + ')}${missing.length === 0 ? ' (already held)' : ''}`);
|
|
141
|
+
}
|
|
142
|
+
logger.info(`persona sync: ${accounts} account(s) in '${environment}', ${granted} role grant(s) applied, ${held} already held`);
|
|
143
|
+
if (skipped.length > 0) {
|
|
144
|
+
logger.info(` ${skipped.length} persona(s) skipped — they do not act in '${environment}'. Run with --dry-run for the reasons.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await opened.destroy();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run one declared persona against a real stage as a virtual user.
|
|
3
|
+
*
|
|
4
|
+
* Everything it needs is already in the project: the catalogue is the function
|
|
5
|
+
* meta, the intents are the scenarios' own prose, the identity is the persona
|
|
6
|
+
* signing in over the app's real auth, and the scopes come from the roles they
|
|
7
|
+
* declared. The only new input is which person to be — everything else is a
|
|
8
|
+
* flag for narrowing or replaying a run.
|
|
9
|
+
*
|
|
10
|
+
* This is deliberately not a test runner. It asserts nothing, and a green run
|
|
11
|
+
* proves nothing — what it produces is findings, and the absence of them is
|
|
12
|
+
* only ever "not this time, not with this seed".
|
|
13
|
+
*/
|
|
14
|
+
export declare const personaRun: import("#pikku").PikkuFunctionConfig<{
|
|
15
|
+
environment: string;
|
|
16
|
+
persona: string;
|
|
17
|
+
disposition?: string;
|
|
18
|
+
goals?: string;
|
|
19
|
+
steps?: string;
|
|
20
|
+
mutations?: string;
|
|
21
|
+
duration?: string;
|
|
22
|
+
seed?: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
apiUrl?: string;
|
|
25
|
+
allowApproval?: boolean;
|
|
26
|
+
skipRoleCheck?: boolean;
|
|
27
|
+
out?: string;
|
|
28
|
+
}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
|
|
29
|
+
environment: string;
|
|
30
|
+
persona: string;
|
|
31
|
+
disposition?: string;
|
|
32
|
+
goals?: string;
|
|
33
|
+
steps?: string;
|
|
34
|
+
mutations?: string;
|
|
35
|
+
duration?: string;
|
|
36
|
+
seed?: string;
|
|
37
|
+
model?: string;
|
|
38
|
+
apiUrl?: string;
|
|
39
|
+
allowApproval?: boolean;
|
|
40
|
+
skipRoleCheck?: boolean;
|
|
41
|
+
out?: string;
|
|
42
|
+
}, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
|
|
43
|
+
environment: string;
|
|
44
|
+
persona: string;
|
|
45
|
+
disposition?: string;
|
|
46
|
+
goals?: string;
|
|
47
|
+
steps?: string;
|
|
48
|
+
mutations?: string;
|
|
49
|
+
duration?: string;
|
|
50
|
+
seed?: string;
|
|
51
|
+
model?: string;
|
|
52
|
+
apiUrl?: string;
|
|
53
|
+
allowApproval?: boolean;
|
|
54
|
+
skipRoleCheck?: boolean;
|
|
55
|
+
out?: string;
|
|
56
|
+
}, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|
|
57
|
+
/** Who this app is for, and what each of them wants, without running anything. */
|
|
58
|
+
export declare const personaList: import("#pikku").PikkuFunctionConfig<void, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<void, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|