@pikku/cli 0.12.91 → 0.12.93
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/LICENSE +106 -0
- package/console-app/assets/{index-D0HG8q0B.css → index-BYj7ZbUU.css} +1 -1
- package/console-app/assets/{index-CpkvmAGb.js → index-CmAplHjZ.js} +151 -149
- 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.js +16 -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-contracts-meta.gen.json +15 -0
- 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 +48 -0
- 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 +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 +73 -40
- package/dist/.pikku/function/pikku-functions.gen.js +1 -3
- 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 +2 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +2 -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 +3 -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-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 +4 -0
- package/dist/.pikku/scenarios/schemas/register.gen.js +4 -0
- 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/FabricSecretsListOutput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricSecretsRotateInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricSecretsRotateOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricSecretsSetOutput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/KnowledgeIndexInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/KnowledgeIndexOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/KnowledgeValidateInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/KnowledgeValidateOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +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/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/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 +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/meta/allWorkflow.gen.json +2 -8
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.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/cli.wiring.js +29 -0
- package/dist/src/deploy/analyzer/analyzer.js +22 -5
- package/dist/src/deploy/build-pipeline.js +5 -1
- package/dist/src/fabric/fabric-commands.d.ts +37 -9
- package/dist/src/fabric/fabric-commands.js +12 -0
- package/dist/src/fabric/functions/domains-add.function.d.ts +4 -4
- package/dist/src/fabric/functions/secrets-list.function.d.ts +16 -4
- package/dist/src/fabric/functions/secrets-list.function.js +12 -10
- package/dist/src/fabric/functions/secrets-rotate.function.d.ts +24 -0
- package/dist/src/fabric/functions/secrets-rotate.function.js +36 -0
- package/dist/src/fabric/functions/secrets-set.function.d.ts +8 -4
- package/dist/src/fabric/functions/secrets-set.function.js +18 -7
- package/dist/src/fabric/functions/validate.function.js +16 -7
- package/dist/src/fabric/lib/http.d.ts +7 -1
- package/dist/src/fabric/lib/sealed-box.d.ts +16 -0
- package/dist/src/fabric/lib/sealed-box.js +72 -0
- package/dist/src/functions/commands/dev.js +9 -0
- package/dist/src/functions/commands/knowledge-index.d.ts +30 -0
- package/dist/src/functions/commands/knowledge-index.js +11 -0
- package/dist/src/functions/commands/knowledge-validate.d.ts +33 -0
- package/dist/src/functions/commands/knowledge-validate.js +11 -0
- package/dist/src/functions/commands/new-addon.d.ts +18 -0
- package/dist/src/functions/commands/new-addon.js +40 -24
- package/dist/src/functions/commands/pikku-command-bootstrap.js +1 -0
- package/dist/src/functions/commands/scenario-browser.d.ts +17 -2
- package/dist/src/functions/commands/scenario-browser.js +16 -8
- package/dist/src/functions/commands/scenario-environment.d.ts +32 -0
- package/dist/src/functions/commands/scenario-environment.js +61 -0
- package/dist/src/functions/commands/scenario.d.ts +6 -0
- package/dist/src/functions/commands/scenario.js +14 -9
- package/dist/src/functions/commands/skills.js +27 -61
- package/dist/src/functions/db/db-codegen.d.ts +6 -0
- package/dist/src/functions/db/db-codegen.js +9 -0
- package/dist/src/functions/db/db-migrator.js +14 -0
- package/dist/src/functions/db/local-db.d.ts +8 -0
- package/dist/src/functions/db/local-db.js +24 -2
- package/dist/src/functions/db/migration-identifiers.d.ts +58 -0
- package/dist/src/functions/db/migration-identifiers.js +262 -0
- package/dist/src/functions/db/migration-provenance.d.ts +35 -0
- package/dist/src/functions/db/migration-provenance.js +80 -0
- package/dist/src/functions/db/schema-sql.d.ts +43 -0
- package/dist/src/functions/db/schema-sql.js +135 -0
- package/dist/src/functions/db/sqlite/sqlite-kysely.js +22 -2
- package/dist/src/functions/knowledge/render.d.ts +3 -0
- package/dist/src/functions/knowledge/render.js +66 -0
- package/dist/src/functions/knowledge/schemas.d.ts +51 -0
- package/dist/src/functions/knowledge/schemas.js +25 -0
- package/dist/src/functions/wirings/credentials/serialize-credentials-types.js +8 -1
- package/dist/src/functions/wirings/functions/schemas.js +13 -1
- package/dist/src/functions/wirings/rpc/pikku-command-rpc.js +6 -1
- package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +10 -0
- package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +102 -0
- package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +13 -0
- package/dist/src/functions/wirings/scenarios/scenario-partition.js +15 -2
- package/dist/src/functions/wirings/scenarios/scenario-schema-partition.d.ts +33 -0
- package/dist/src/functions/wirings/scenarios/scenario-schema-partition.js +49 -0
- package/dist/src/functions/wirings/secrets/serialize-secrets-types.js +10 -3
- package/dist/src/functions/wirings/variables/serialize-variables-types.js +8 -1
- package/dist/src/functions/workflows/all.workflow.js +2 -7
- package/dist/src/utils/file-writer.js +14 -2
- package/dist/src/utils/pikku-cli-config.d.ts +14 -0
- package/dist/src/utils/pikku-cli-config.js +34 -0
- package/dist/src/utils/remove-legacy-scaffold-file.d.ts +14 -0
- package/dist/src/utils/remove-legacy-scaffold-file.js +23 -0
- package/dist/src/utils/serialize-schemas.d.ts +3 -1
- package/dist/src/utils/serialize-schemas.js +39 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -9
- package/dist/.pikku/schemas/schemas/PikkuScenarioFunctionsOutput.schema.json +0 -1
- package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +0 -1
- package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +0 -31
- package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +0 -10
- package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +0 -104
- package/skills/pikku-addon/SKILL.md +0 -243
- package/skills/pikku-addon/references/addon-package-manifest.md +0 -63
- package/skills/pikku-ai-agent/SKILL.md +0 -231
- package/skills/pikku-ai-vercel/SKILL.md +0 -82
- package/skills/pikku-ai-voice/SKILL.md +0 -88
- package/skills/pikku-audit/SKILL.md +0 -175
- package/skills/pikku-aws/SKILL.md +0 -111
- package/skills/pikku-backblaze/SKILL.md +0 -71
- package/skills/pikku-better-auth/SKILL.md +0 -298
- package/skills/pikku-cli/SKILL.md +0 -198
- package/skills/pikku-cli/references/complete-example.md +0 -82
- package/skills/pikku-concepts/SKILL.md +0 -250
- package/skills/pikku-concepts/references/concept-mapping.md +0 -556
- package/skills/pikku-concepts/references/packages.md +0 -29
- package/skills/pikku-config/SKILL.md +0 -212
- package/skills/pikku-cron/SKILL.md +0 -214
- package/skills/pikku-deploy-azure/SKILL.md +0 -71
- package/skills/pikku-deploy-cloudflare/SKILL.md +0 -82
- package/skills/pikku-deploy-express/SKILL.md +0 -86
- package/skills/pikku-deploy-fastify/SKILL.md +0 -73
- package/skills/pikku-deploy-lambda/SKILL.md +0 -110
- package/skills/pikku-deploy-nextjs/SKILL.md +0 -78
- package/skills/pikku-deploy-uws/SKILL.md +0 -88
- package/skills/pikku-deps/SKILL.md +0 -98
- package/skills/pikku-emails/SKILL.md +0 -163
- package/skills/pikku-fabric/SKILL.md +0 -318
- package/skills/pikku-fabric-debug/SKILL.md +0 -112
- package/skills/pikku-feature/SKILL.md +0 -258
- package/skills/pikku-gateway-slack/SKILL.md +0 -115
- package/skills/pikku-http/SKILL.md +0 -220
- package/skills/pikku-http/references/http-options.md +0 -55
- package/skills/pikku-i18n/SKILL.md +0 -137
- package/skills/pikku-info/SKILL.md +0 -100
- package/skills/pikku-jose/SKILL.md +0 -105
- package/skills/pikku-kysely/SKILL.md +0 -219
- package/skills/pikku-machine-auth/SKILL.md +0 -183
- package/skills/pikku-mcp/SKILL.md +0 -241
- package/skills/pikku-middleware/SKILL.md +0 -231
- package/skills/pikku-middleware/references/middleware-patterns.md +0 -61
- package/skills/pikku-mongodb/SKILL.md +0 -105
- package/skills/pikku-n8n-import/SKILL.md +0 -109
- package/skills/pikku-n8n-import/SPEC.md +0 -84
- package/skills/pikku-n8n-import/references/addon-mapping.md +0 -121
- package/skills/pikku-n8n-import/references/code-translation.md +0 -121
- package/skills/pikku-n8n-import/references/loops-and-control.md +0 -87
- package/skills/pikku-paraglide/SKILL.md +0 -117
- package/skills/pikku-permissions/SKILL.md +0 -192
- package/skills/pikku-pino/SKILL.md +0 -79
- package/skills/pikku-product-second-opinion/README.md +0 -43
- package/skills/pikku-product-second-opinion/SKILL.md +0 -158
- package/skills/pikku-product-second-opinion/example/sample-report.md +0 -90
- package/skills/pikku-product-second-opinion/references/report-template.md +0 -73
- package/skills/pikku-queue/SKILL.md +0 -240
- package/skills/pikku-react/SKILL.md +0 -212
- package/skills/pikku-react-query/SKILL.md +0 -242
- package/skills/pikku-realtime/SKILL.md +0 -236
- package/skills/pikku-realtime/references/other-routes.md +0 -23
- package/skills/pikku-redis/SKILL.md +0 -90
- package/skills/pikku-rpc/SKILL.md +0 -171
- package/skills/pikku-rtl/SKILL.md +0 -219
- package/skills/pikku-scenario/SKILL.md +0 -382
- package/skills/pikku-schedule/SKILL.md +0 -57
- package/skills/pikku-schema-ajv/SKILL.md +0 -62
- package/skills/pikku-schema-cfworker/SKILL.md +0 -63
- package/skills/pikku-security/SKILL.md +0 -108
- package/skills/pikku-services/SKILL.md +0 -248
- package/skills/pikku-services/references/audit-wire-service.md +0 -34
- package/skills/pikku-software-archaeology/README.md +0 -70
- package/skills/pikku-software-archaeology/SKILL.md +0 -186
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +0 -625
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +0 -49
- package/skills/pikku-software-archaeology/scripts/validate.mjs +0 -173
- package/skills/pikku-tag-middleware/SKILL.md +0 -13
- package/skills/pikku-template-clone/SKILL.md +0 -40
- package/skills/pikku-trigger/SKILL.md +0 -181
- package/skills/pikku-versioning/SKILL.md +0 -173
- package/skills/pikku-websocket/SKILL.md +0 -243
- package/skills/pikku-workflow/SKILL.md +0 -172
- package/skills/pikku-workflow/references/workflow-reference.md +0 -63
- package/skills/pikku-workflows-client/SKILL.md +0 -150
- package/skills/pikku-ws/SKILL.md +0 -47
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sealing a value to a stage's public key, here on the client.
|
|
3
|
+
*
|
|
4
|
+
* `pikku fabric secrets set` seals before it sends, so the plaintext never
|
|
5
|
+
* reaches fabric — not the ingress in front of it, not a request log, not the
|
|
6
|
+
* process handling the write. Fabric stores the sealed blob and hands it to the
|
|
7
|
+
* worker, which holds the only private key that opens it.
|
|
8
|
+
*
|
|
9
|
+
* This is the seal half of fabric's `sealed-x25519-v1`, and the two must stay
|
|
10
|
+
* byte-compatible: an ephemeral X25519 keypair, ECDH against the recipient,
|
|
11
|
+
* HKDF-SHA256 to a content key, AES-256-GCM to seal. Everything below is
|
|
12
|
+
* `node:crypto` on purpose — a sealing primitive that pulled in a dependency
|
|
13
|
+
* would be a supply-chain question sitting directly on the plaintext.
|
|
14
|
+
*
|
|
15
|
+
* There is deliberately no `unseal` here. The CLI has no private key and no
|
|
16
|
+
* business having one; opening a value happens in the worker.
|
|
17
|
+
*/
|
|
18
|
+
import { createCipheriv, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, } from 'node:crypto';
|
|
19
|
+
/** Algorithm tag carried by every sealed value. Must match fabric's. */
|
|
20
|
+
export const SEALED_BOX_ALGORITHM = 'sealed-x25519-v1';
|
|
21
|
+
/** HKDF domain separator. Must match fabric's byte for byte. */
|
|
22
|
+
const SEAL_SALT = 'pikkufabric/sealed-box/v1';
|
|
23
|
+
/** Seal a value to a stage's public key. */
|
|
24
|
+
export function seal(publicKey, keyId, plaintext) {
|
|
25
|
+
const recipient = importPublicKey(publicKey);
|
|
26
|
+
const ephemeral = generateKeyPairSync('x25519');
|
|
27
|
+
const epk = ephemeral.publicKey.export({ type: 'spki', format: 'der' });
|
|
28
|
+
const contentKey = deriveContentKey(diffieHellman({ privateKey: ephemeral.privateKey, publicKey: recipient }), epk, recipient.export({ type: 'spki', format: 'der' }));
|
|
29
|
+
const iv = randomBytes(12);
|
|
30
|
+
const cipher = createCipheriv('aes-256-gcm', contentKey, iv);
|
|
31
|
+
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
32
|
+
return {
|
|
33
|
+
alg: SEALED_BOX_ALGORITHM,
|
|
34
|
+
kid: keyId,
|
|
35
|
+
epk: epk.toString('base64'),
|
|
36
|
+
iv: iv.toString('base64'),
|
|
37
|
+
ct: ct.toString('base64'),
|
|
38
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Wire form. Fabric stores exactly this string. */
|
|
42
|
+
export function serializeSealedValue(sealed) {
|
|
43
|
+
return JSON.stringify(sealed);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Both public keys go into the HKDF `info`, which is what stops a sealed value
|
|
47
|
+
* being redirected: change either the ephemeral key or the recipient and the
|
|
48
|
+
* derived content key changes with it.
|
|
49
|
+
*/
|
|
50
|
+
function deriveContentKey(shared, epk, recipientPublic) {
|
|
51
|
+
return Buffer.from(hkdfSync('sha256', shared, Buffer.from(SEAL_SALT), Buffer.concat([epk, recipientPublic]), 32));
|
|
52
|
+
}
|
|
53
|
+
function importPublicKey(publicKey) {
|
|
54
|
+
let key;
|
|
55
|
+
try {
|
|
56
|
+
key = createPublicKey({
|
|
57
|
+
key: Buffer.from(publicKey, 'base64'),
|
|
58
|
+
type: 'spki',
|
|
59
|
+
format: 'der',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new Error(`Not a valid X25519 public key (${error instanceof Error ? error.message : String(error)})`);
|
|
64
|
+
}
|
|
65
|
+
// A public key of the wrong curve would still ECDH, just against a key the
|
|
66
|
+
// stage's worker cannot match — the failure would surface as an unopenable
|
|
67
|
+
// secret much later, so refuse it at the point the type is still knowable.
|
|
68
|
+
if (key.asymmetricKeyType !== 'x25519') {
|
|
69
|
+
throw new Error(`Stage sealing key is ${key.asymmetricKeyType}, expected x25519 — this CLI is too old for it, run \`npm i -g @pikku/cli\``);
|
|
70
|
+
}
|
|
71
|
+
return key;
|
|
72
|
+
}
|
|
@@ -11,6 +11,7 @@ import { LocalContent, } from '@pikku/core/services/local-content';
|
|
|
11
11
|
import { InMemorySchedulerService } from '@pikku/schedule';
|
|
12
12
|
import { resolveDb, createKysely, parseDatabaseUrl, } from '../db/local-db.js';
|
|
13
13
|
import { loadUserBootstrap, loadUserModule } from './load-user-project.js';
|
|
14
|
+
import { registerScenarioInstrumentation } from '../wirings/scenarios/register-scenario-instrumentation.js';
|
|
14
15
|
import { startCoverageService } from './start-coverage.js';
|
|
15
16
|
import { createDevAIAgentRunner } from './dev-ai-runner.js';
|
|
16
17
|
import { resolveConsoleMount } from './serve-console.js';
|
|
@@ -115,6 +116,14 @@ export const dev = pikkuSessionlessFunc({
|
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
117
118
|
await loadUserBootstrap(pikkuDir);
|
|
119
|
+
// Scenario instrumentation exists only on a development server: the runner
|
|
120
|
+
// calls it over RPC to reset and snapshot coverage and stub calls. It is
|
|
121
|
+
// registered here, after the app bootstrap, rather than generated into the
|
|
122
|
+
// project — an app bootstrap is what a deployed bundle imports, and coverage
|
|
123
|
+
// endpoints have no business in one.
|
|
124
|
+
if (config.scaffold?.scenarios) {
|
|
125
|
+
registerScenarioInstrumentation(config.scaffold.scenarios === 'auth');
|
|
126
|
+
}
|
|
118
127
|
const configModule = await loadUserModule(pikkuConfigFactory.file);
|
|
119
128
|
const servicesModule = await loadUserModule(singletonServicesFactory.file);
|
|
120
129
|
const userCreateConfig = configModule[pikkuConfigFactory.variable];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { renderKnowledgeIndex } from '../knowledge/render.js';
|
|
2
|
+
export declare const knowledgeIndex: import("#pikku").PikkuFunctionConfig<{
|
|
3
|
+
check?: boolean | undefined;
|
|
4
|
+
}, {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
check: boolean;
|
|
7
|
+
files: {
|
|
8
|
+
path: string;
|
|
9
|
+
action: "unchanged" | "created" | "updated";
|
|
10
|
+
}[];
|
|
11
|
+
}, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
|
|
12
|
+
check?: boolean | undefined;
|
|
13
|
+
}, {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
check: boolean;
|
|
16
|
+
files: {
|
|
17
|
+
path: string;
|
|
18
|
+
action: "unchanged" | "created" | "updated";
|
|
19
|
+
}[];
|
|
20
|
+
}, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
|
|
21
|
+
check?: boolean | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
check: boolean;
|
|
25
|
+
files: {
|
|
26
|
+
path: string;
|
|
27
|
+
action: "unchanged" | "created" | "updated";
|
|
28
|
+
}[];
|
|
29
|
+
}, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|
|
30
|
+
export { renderKnowledgeIndex };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
+
import { runKnowledgeIndex } from '@pikku/knowledge';
|
|
3
|
+
import { renderKnowledgeIndex } from '../knowledge/render.js';
|
|
4
|
+
import { KnowledgeIndexInputSchema, KnowledgeIndexOutputSchema, } from '../knowledge/schemas.js';
|
|
5
|
+
export const knowledgeIndex = pikkuSessionlessFunc({
|
|
6
|
+
description: "Refresh every index.md so each section lists the notes actually in it, replacing only the generated block and leaving each index's prose alone.",
|
|
7
|
+
input: KnowledgeIndexInputSchema,
|
|
8
|
+
output: KnowledgeIndexOutputSchema,
|
|
9
|
+
func: async ({ config }, { check }) => runKnowledgeIndex(config.rootDir, check ?? false),
|
|
10
|
+
});
|
|
11
|
+
export { renderKnowledgeIndex };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { renderKnowledgeValidate } from '../knowledge/render.js';
|
|
2
|
+
export declare const knowledgeValidate: import("#pikku").PikkuFunctionConfig<Record<string, never>, {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
notes: number;
|
|
5
|
+
findings: {
|
|
6
|
+
id: string;
|
|
7
|
+
severity: "warn" | "error" | "info";
|
|
8
|
+
message: string;
|
|
9
|
+
path: string;
|
|
10
|
+
fixHint: string;
|
|
11
|
+
}[];
|
|
12
|
+
}, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<Record<string, never>, {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
notes: number;
|
|
15
|
+
findings: {
|
|
16
|
+
id: string;
|
|
17
|
+
severity: "warn" | "error" | "info";
|
|
18
|
+
message: string;
|
|
19
|
+
path: string;
|
|
20
|
+
fixHint: string;
|
|
21
|
+
}[];
|
|
22
|
+
}, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<Record<string, never>, {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
notes: number;
|
|
25
|
+
findings: {
|
|
26
|
+
id: string;
|
|
27
|
+
severity: "warn" | "error" | "info";
|
|
28
|
+
message: string;
|
|
29
|
+
path: string;
|
|
30
|
+
fixHint: string;
|
|
31
|
+
}[];
|
|
32
|
+
}, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|
|
33
|
+
export { renderKnowledgeValidate };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
+
import { runKnowledgeValidate } from '@pikku/knowledge';
|
|
3
|
+
import { renderKnowledgeValidate } from '../knowledge/render.js';
|
|
4
|
+
import { KnowledgeValidateInputSchema, KnowledgeValidateOutputSchema, } from '../knowledge/schemas.js';
|
|
5
|
+
export const knowledgeValidate = pikkuSessionlessFunc({
|
|
6
|
+
description: 'Check the knowledge base against the app-project profile: every note typed, every section indexed, every slice gated, and every resource: pointing at something that still exists.',
|
|
7
|
+
input: KnowledgeValidateInputSchema,
|
|
8
|
+
output: KnowledgeValidateOutputSchema,
|
|
9
|
+
func: async ({ config }) => runKnowledgeValidate(config.rootDir, config.outDir),
|
|
10
|
+
});
|
|
11
|
+
export { renderKnowledgeValidate };
|
|
@@ -9,6 +9,24 @@
|
|
|
9
9
|
* the only option there.
|
|
10
10
|
*/
|
|
11
11
|
export declare function resolveAddonDepProtocol(baseDir: string): string;
|
|
12
|
+
export interface AddonVars {
|
|
13
|
+
name: string;
|
|
14
|
+
camelName: string;
|
|
15
|
+
pascalName: string;
|
|
16
|
+
screamingName: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
description: string;
|
|
19
|
+
category: string;
|
|
20
|
+
addonDepProtocol: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function getAddonFiles(vars: AddonVars, flags: {
|
|
23
|
+
secret: boolean;
|
|
24
|
+
variable: boolean;
|
|
25
|
+
oauth: boolean;
|
|
26
|
+
credential?: 'apikey' | 'bearer' | 'oauth2';
|
|
27
|
+
/** Delegated login: credential is the upstream token + expiry, checked per call. */
|
|
28
|
+
delegated?: boolean;
|
|
29
|
+
}): Record<string, string>;
|
|
12
30
|
export declare const pikkuNewAddon: import("#pikku").PikkuFunctionConfig<{
|
|
13
31
|
name: string;
|
|
14
32
|
displayName?: string;
|
|
@@ -110,7 +110,23 @@ function sanitizeAddonName(raw) {
|
|
|
110
110
|
name = `x${name}`;
|
|
111
111
|
return name || raw;
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* `--display-name` and `--description` are free-form prose, so an apostrophe in
|
|
115
|
+
* them is ordinary — "Bob's CRM". `JSON.stringify` emits a valid, escaped
|
|
116
|
+
* string literal; interpolating the raw text into a quoted one terminates the
|
|
117
|
+
* string and the scaffolded file no longer parses. Prose composed with other
|
|
118
|
+
* words is stringified as a whole, not swapped in place.
|
|
119
|
+
*/
|
|
120
|
+
const literal = (value) => JSON.stringify(value);
|
|
121
|
+
/**
|
|
122
|
+
* The same prose inside a generated template literal, where a backtick or a
|
|
123
|
+
* `${` opens an interpolation rather than ending the string — so the emitted
|
|
124
|
+
* message keeps its own `\${response.status}` holes.
|
|
125
|
+
*/
|
|
126
|
+
const inTemplate = (value) => value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
127
|
+
/** The same prose on a `//` comment line, where a newline ends the comment. */
|
|
128
|
+
const inComment = (value) => value.replace(/\s+/g, ' ').trim();
|
|
129
|
+
export function getAddonFiles(vars, flags) {
|
|
114
130
|
const { name, camelName, pascalName, screamingName, displayName, description, category, } = vars;
|
|
115
131
|
const files = {};
|
|
116
132
|
// package.json
|
|
@@ -205,7 +221,7 @@ ${description}
|
|
|
205
221
|
6. Build: \`yarn install && yarn pikku && yarn build\`
|
|
206
222
|
`;
|
|
207
223
|
// src/index.ts
|
|
208
|
-
files['src/index.ts'] = `// ${displayName} functions
|
|
224
|
+
files['src/index.ts'] = `// ${inComment(displayName)} functions
|
|
209
225
|
// export { ${camelName}Operation } from './functions/operation.function.js'
|
|
210
226
|
`;
|
|
211
227
|
// src/services.ts
|
|
@@ -224,10 +240,10 @@ export const createWireServices = pikkuAddonWireServices(
|
|
|
224
240
|
}
|
|
225
241
|
const cred = await wire.getCredential<{ token: string; expiresAt?: number }>('${camelName}')
|
|
226
242
|
if (!cred?.token) {
|
|
227
|
-
throw new UnauthorizedError(
|
|
243
|
+
throw new UnauthorizedError(${literal(`No ${displayName} session — sign in again`)})
|
|
228
244
|
}
|
|
229
245
|
if (cred.expiresAt && cred.expiresAt * 1000 < Date.now()) {
|
|
230
|
-
throw new UnauthorizedError(
|
|
246
|
+
throw new UnauthorizedError(${literal(`${displayName} session expired — sign in again`)})
|
|
231
247
|
}
|
|
232
248
|
const ${camelName} = new ${pascalName}Service(cred, variables)
|
|
233
249
|
|
|
@@ -275,7 +291,7 @@ export const createWireServices = pikkuAddonWireServices(
|
|
|
275
291
|
}
|
|
276
292
|
const cred = await wire.getCredential<{ accessToken: string }>('${camelName}')
|
|
277
293
|
if (!cred?.accessToken) {
|
|
278
|
-
throw new UnauthorizedError(
|
|
294
|
+
throw new UnauthorizedError(${literal(`No ${displayName} connection — connect ${displayName} first`)})
|
|
279
295
|
}
|
|
280
296
|
const ${camelName} = new ${pascalName}Service(cred, variables)
|
|
281
297
|
|
|
@@ -363,7 +379,7 @@ export class ${pascalName}Service {
|
|
|
363
379
|
|
|
364
380
|
if (!response.ok) {
|
|
365
381
|
const errorText = await response.text()
|
|
366
|
-
throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)
|
|
382
|
+
throw new Error(\`${inTemplate(displayName)} API error (\${response.status}): \${errorText}\`)
|
|
367
383
|
}
|
|
368
384
|
|
|
369
385
|
return response.json() as Promise<T>
|
|
@@ -416,7 +432,7 @@ export class ${pascalName}Service {
|
|
|
416
432
|
|
|
417
433
|
if (!response.ok) {
|
|
418
434
|
const errorText = await response.text()
|
|
419
|
-
throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)
|
|
435
|
+
throw new Error(\`${inTemplate(displayName)} API error (\${response.status}): \${errorText}\`)
|
|
420
436
|
}
|
|
421
437
|
|
|
422
438
|
return response.json() as Promise<T>
|
|
@@ -464,7 +480,7 @@ export class ${pascalName}Service {
|
|
|
464
480
|
|
|
465
481
|
if (!response.ok) {
|
|
466
482
|
const errorText = await response.text()
|
|
467
|
-
throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)
|
|
483
|
+
throw new Error(\`${inTemplate(displayName)} API error (\${response.status}): \${errorText}\`)
|
|
468
484
|
}
|
|
469
485
|
|
|
470
486
|
return response.json() as Promise<T>
|
|
@@ -507,7 +523,7 @@ export class ${pascalName}Service {
|
|
|
507
523
|
|
|
508
524
|
if (!response.ok) {
|
|
509
525
|
const errorText = await response.text()
|
|
510
|
-
throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)
|
|
526
|
+
throw new Error(\`${inTemplate(displayName)} API error (\${response.status}): \${errorText}\`)
|
|
511
527
|
}
|
|
512
528
|
|
|
513
529
|
return response.json() as Promise<T>
|
|
@@ -553,13 +569,13 @@ export interface Services extends CoreServices<SingletonServices> {}
|
|
|
553
569
|
import { wireCredential } from '@pikku/core/credential'
|
|
554
570
|
|
|
555
571
|
export const ${camelName}CredentialSchema = z.object({
|
|
556
|
-
apiKey: z.string().describe(
|
|
572
|
+
apiKey: z.string().describe(${literal(`${displayName} API key`)}),
|
|
557
573
|
})
|
|
558
574
|
|
|
559
575
|
wireCredential({
|
|
560
576
|
name: '${camelName}',
|
|
561
|
-
displayName:
|
|
562
|
-
description:
|
|
577
|
+
displayName: ${literal(displayName)},
|
|
578
|
+
description: ${literal(description)},
|
|
563
579
|
type: 'wire',
|
|
564
580
|
schema: ${camelName}CredentialSchema,
|
|
565
581
|
})
|
|
@@ -567,10 +583,10 @@ wireCredential({
|
|
|
567
583
|
}
|
|
568
584
|
else if (flags.credential === 'bearer') {
|
|
569
585
|
const credentialFields = flags.delegated
|
|
570
|
-
? ` token: z.string().describe(
|
|
586
|
+
? ` token: z.string().describe(${literal(`${displayName} session token captured at delegated sign-in`)}),
|
|
571
587
|
expiresAt: z.number().optional().describe('Token expiry (epoch seconds)'),
|
|
572
588
|
tenantId: z.string().optional().describe('Upstream tenant id'),`
|
|
573
|
-
: ` token: z.string().describe(
|
|
589
|
+
: ` token: z.string().describe(${literal(`${displayName} bearer token`)}),`;
|
|
574
590
|
files[`src/${name}.credential.ts`] = `import { z } from 'zod'
|
|
575
591
|
import { wireCredential } from '@pikku/core/credential'
|
|
576
592
|
|
|
@@ -580,8 +596,8 @@ ${credentialFields}
|
|
|
580
596
|
|
|
581
597
|
wireCredential({
|
|
582
598
|
name: '${camelName}',
|
|
583
|
-
displayName:
|
|
584
|
-
description:
|
|
599
|
+
displayName: ${literal(displayName)},
|
|
600
|
+
description: ${literal(description)},
|
|
585
601
|
type: 'wire',
|
|
586
602
|
schema: ${camelName}CredentialSchema,
|
|
587
603
|
})
|
|
@@ -599,8 +615,8 @@ export const ${camelName}TokenSchema = z.object({
|
|
|
599
615
|
|
|
600
616
|
wireCredential({
|
|
601
617
|
name: '${camelName}',
|
|
602
|
-
displayName:
|
|
603
|
-
description:
|
|
618
|
+
displayName: ${literal(displayName)},
|
|
619
|
+
description: ${literal(description)},
|
|
604
620
|
type: 'wire',
|
|
605
621
|
schema: ${camelName}TokenSchema,
|
|
606
622
|
oauth2: {
|
|
@@ -614,8 +630,8 @@ wireCredential({
|
|
|
614
630
|
|
|
615
631
|
wireSecret({
|
|
616
632
|
name: '${camelName}OAuthApp',
|
|
617
|
-
displayName:
|
|
618
|
-
description:
|
|
633
|
+
displayName: ${literal(`${displayName} OAuth App`)},
|
|
634
|
+
description: ${literal(`OAuth2 app credentials for ${displayName}`)},
|
|
619
635
|
secretId: '${screamingName}_OAUTH_APP',
|
|
620
636
|
})
|
|
621
637
|
`;
|
|
@@ -625,7 +641,7 @@ wireSecret({
|
|
|
625
641
|
import { wireSecret } from '@pikku/core/secret'
|
|
626
642
|
|
|
627
643
|
export const ${camelName}SecretsSchema = z.object({
|
|
628
|
-
apiKey: z.string().describe(
|
|
644
|
+
apiKey: z.string().describe(${literal(`${displayName} API key`)}),
|
|
629
645
|
// Add other secret fields as needed
|
|
630
646
|
})
|
|
631
647
|
|
|
@@ -633,8 +649,8 @@ export type ${pascalName}Secrets = z.infer<typeof ${camelName}SecretsSchema>
|
|
|
633
649
|
|
|
634
650
|
wireSecret({
|
|
635
651
|
name: '${camelName}',
|
|
636
|
-
displayName:
|
|
637
|
-
description:
|
|
652
|
+
displayName: ${literal(`${displayName} API`)},
|
|
653
|
+
description: ${literal(description)},
|
|
638
654
|
secretId: '${screamingName}_CREDENTIALS',
|
|
639
655
|
schema: ${camelName}SecretsSchema,
|
|
640
656
|
})
|
|
@@ -649,7 +665,7 @@ export const ${camelName}VariableSchema = z.string().optional().describe('TODO:
|
|
|
649
665
|
|
|
650
666
|
wireVariable({
|
|
651
667
|
name: '${camelName}_variable',
|
|
652
|
-
displayName:
|
|
668
|
+
displayName: ${literal(`${displayName} Variable`)},
|
|
653
669
|
description: 'TODO: describe this variable',
|
|
654
670
|
variableId: '${screamingName}_VARIABLE',
|
|
655
671
|
schema: ${camelName}VariableSchema,
|
|
@@ -70,6 +70,7 @@ export const pikkuBootstrap = pikkuSessionlessFunc({
|
|
|
70
70
|
// in place however this module is reached.
|
|
71
71
|
const scenarioBootstrapImports = [
|
|
72
72
|
config.bootstrapFile,
|
|
73
|
+
join(config.scenarioSchemaDirectory, 'register.gen.ts'),
|
|
73
74
|
config.scenarioStepsMetaFile,
|
|
74
75
|
config.scenarioStepsFile,
|
|
75
76
|
config.scenarioWiringsMetaFile,
|
|
@@ -29,9 +29,17 @@ export interface ScenarioBrowserDriver {
|
|
|
29
29
|
createScenarioBrowserProvider?: (options: ScenarioBrowserDriverOptions) => ScenarioBrowserProvider;
|
|
30
30
|
/** The class form the bundled driver exports. */
|
|
31
31
|
PlaywrightScenarioBrowserProvider?: new (options: ScenarioBrowserDriverOptions) => ScenarioBrowserProvider;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Driver-specific config (headed, slowMo, timeouts) resolved from the env.
|
|
34
|
+
*
|
|
35
|
+
* A driver may resolve `appUrl` itself when the caller passes none — that is
|
|
36
|
+
* how a target known only at run time (a sandbox hostname in the env) is
|
|
37
|
+
* reached without a config file per deployment. A driver that answers with a
|
|
38
|
+
* placeholder rather than a real target must say so with
|
|
39
|
+
* `appUrlSource: 'default'`, which the runner reports as a missing appUrl.
|
|
40
|
+
*/
|
|
33
41
|
browserConfigFromEnv?: (overrides: {
|
|
34
|
-
appUrl
|
|
42
|
+
appUrl?: string;
|
|
35
43
|
apiUrl: string;
|
|
36
44
|
}) => unknown;
|
|
37
45
|
}
|
|
@@ -55,6 +63,13 @@ export interface ResolveScenarioBrowserProviderOptions {
|
|
|
55
63
|
* Both failure modes here are discovered before the first scenario starts. A
|
|
56
64
|
* missing `appUrl` or an uninstalled driver found mid-run costs a whole run to
|
|
57
65
|
* learn about.
|
|
66
|
+
*
|
|
67
|
+
* `appUrl` is checked after the driver has resolved its config rather than
|
|
68
|
+
* before, so a driver that knows the target from its own environment (a
|
|
69
|
+
* sandbox hostname) is allowed to supply it. The check is not dropped: a driver
|
|
70
|
+
* reporting `appUrlSource: 'default'` resolved nothing and fails the same way,
|
|
71
|
+
* because a run against a placeholder URL is worse than one that refuses to
|
|
72
|
+
* start.
|
|
58
73
|
*/
|
|
59
74
|
export declare const resolveScenarioBrowserProvider: ({ environment, apiUrl, appUrl, secret, actors, signInPath, failureDir, browserScenarios, driver, importDriver, }: ResolveScenarioBrowserProviderOptions) => Promise<ScenarioBrowserProvider>;
|
|
60
75
|
/**
|
|
@@ -4,12 +4,15 @@ export const DEFAULT_BROWSER_DRIVER = '@pikku/playwright';
|
|
|
4
4
|
* Both failure modes here are discovered before the first scenario starts. A
|
|
5
5
|
* missing `appUrl` or an uninstalled driver found mid-run costs a whole run to
|
|
6
6
|
* learn about.
|
|
7
|
+
*
|
|
8
|
+
* `appUrl` is checked after the driver has resolved its config rather than
|
|
9
|
+
* before, so a driver that knows the target from its own environment (a
|
|
10
|
+
* sandbox hostname) is allowed to supply it. The check is not dropped: a driver
|
|
11
|
+
* reporting `appUrlSource: 'default'` resolved nothing and fails the same way,
|
|
12
|
+
* because a run against a placeholder URL is worse than one that refuses to
|
|
13
|
+
* start.
|
|
7
14
|
*/
|
|
8
15
|
export const resolveScenarioBrowserProvider = async ({ environment, apiUrl, appUrl, secret, actors, signInPath, failureDir, browserScenarios, driver = DEFAULT_BROWSER_DRIVER, importDriver = (specifier) => import(specifier), }) => {
|
|
9
|
-
if (!appUrl) {
|
|
10
|
-
throw new Error(`Scenario environment '${environment}' has browser steps but no 'appUrl'. ` +
|
|
11
|
-
`Add it to scenarios.environments.${environment} in pikku.config.json, or run with --no-browser to skip them.`);
|
|
12
|
-
}
|
|
13
16
|
const module = await importDriver(driver).catch(() => {
|
|
14
17
|
const install = driver === DEFAULT_BROWSER_DRIVER
|
|
15
18
|
? `Run 'yarn add -D ${driver} @playwright/test'`
|
|
@@ -17,15 +20,20 @@ export const resolveScenarioBrowserProvider = async ({ environment, apiUrl, appU
|
|
|
17
20
|
throw new Error(`Scenarios ${browserScenarios.join(', ')} declare browser steps but '${driver}' could not be loaded. ` +
|
|
18
21
|
`${install}, or run with --no-browser to skip them.`);
|
|
19
22
|
});
|
|
23
|
+
const config = (module.browserConfigFromEnv?.({ appUrl, apiUrl }) ?? {
|
|
24
|
+
appUrl,
|
|
25
|
+
apiUrl,
|
|
26
|
+
});
|
|
27
|
+
if (!config.appUrl || config.appUrlSource === 'default') {
|
|
28
|
+
throw new Error(`Scenario environment '${environment}' has browser steps but no 'appUrl', and '${driver}' resolved none from the environment. ` +
|
|
29
|
+
`Add it to scenarios.environments.${environment} in pikku.config.json, pass --app-url for a target that only exists at run time, or run with --no-browser to skip them.`);
|
|
30
|
+
}
|
|
20
31
|
const options = {
|
|
21
32
|
secret,
|
|
22
33
|
actors,
|
|
23
34
|
signInPath,
|
|
24
35
|
failureDir,
|
|
25
|
-
config
|
|
26
|
-
appUrl,
|
|
27
|
-
apiUrl,
|
|
28
|
-
},
|
|
36
|
+
config,
|
|
29
37
|
};
|
|
30
38
|
if (module.createScenarioBrowserProvider) {
|
|
31
39
|
return module.createScenarioBrowserProvider(options);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving `pikku scenario run <environment>` to the URLs the run targets.
|
|
3
|
+
*
|
|
4
|
+
* The configured environment is the base; `--api-url`/`--app-url` override it
|
|
5
|
+
* for one invocation, which is what makes a suite runnable against a target
|
|
6
|
+
* that only exists at run time (a freshly provisioned sandbox) without writing
|
|
7
|
+
* a pikku.config.json per deployment.
|
|
8
|
+
*/
|
|
9
|
+
/** One entry of `scenarios.environments` in pikku.config.json. */
|
|
10
|
+
export interface ScenarioEnvironment {
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
signInPath?: string;
|
|
13
|
+
rpcPath?: string;
|
|
14
|
+
appUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ResolveScenarioEnvironmentOptions {
|
|
17
|
+
/** The name given on the command line. Must exist in config. */
|
|
18
|
+
environment: string;
|
|
19
|
+
environments: Record<string, ScenarioEnvironment>;
|
|
20
|
+
/** `--api-url`, overriding the configured `apiUrl`. */
|
|
21
|
+
apiUrl?: string;
|
|
22
|
+
/** `--app-url`, overriding the configured `appUrl`. */
|
|
23
|
+
appUrl?: string;
|
|
24
|
+
/** `--spawn`, which is only coherent against a local `apiUrl`. */
|
|
25
|
+
spawn?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Applied once, so actors, step env, the browser driver and the spawned server
|
|
29
|
+
* all see the same target. The environment is still looked up by name: the
|
|
30
|
+
* flags override fields on a configured environment, they do not invent one.
|
|
31
|
+
*/
|
|
32
|
+
export declare const resolveScenarioEnvironment: ({ environment, environments, apiUrl, appUrl, spawn, }: ResolveScenarioEnvironmentOptions) => ScenarioEnvironment;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving `pikku scenario run <environment>` to the URLs the run targets.
|
|
3
|
+
*
|
|
4
|
+
* The configured environment is the base; `--api-url`/`--app-url` override it
|
|
5
|
+
* for one invocation, which is what makes a suite runnable against a target
|
|
6
|
+
* that only exists at run time (a freshly provisioned sandbox) without writing
|
|
7
|
+
* a pikku.config.json per deployment.
|
|
8
|
+
*/
|
|
9
|
+
const LOCAL_HOSTNAMES = new Set([
|
|
10
|
+
'localhost',
|
|
11
|
+
'127.0.0.1',
|
|
12
|
+
'0.0.0.0',
|
|
13
|
+
'::1',
|
|
14
|
+
'[::1]',
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Absolute URLs only: the value is concatenated with paths (`${apiUrl}/rpc`)
|
|
18
|
+
* and parsed by the spawn path, so a relative one fails far from its cause.
|
|
19
|
+
*/
|
|
20
|
+
const assertAbsoluteUrl = (flag, value) => {
|
|
21
|
+
let url;
|
|
22
|
+
try {
|
|
23
|
+
url = new URL(value);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error(`${flag} '${value}' is not a valid absolute URL — pass one including the scheme, e.g. ${flag} https://sandbox-a1b2.example.com/api`);
|
|
27
|
+
}
|
|
28
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
29
|
+
throw new Error(`${flag} '${value}' must be an http(s) URL, not '${url.protocol.replace(':', '')}'.`);
|
|
30
|
+
}
|
|
31
|
+
return url;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Applied once, so actors, step env, the browser driver and the spawned server
|
|
35
|
+
* all see the same target. The environment is still looked up by name: the
|
|
36
|
+
* flags override fields on a configured environment, they do not invent one.
|
|
37
|
+
*/
|
|
38
|
+
export const resolveScenarioEnvironment = ({ environment, environments, apiUrl, appUrl, spawn = false, }) => {
|
|
39
|
+
const configured = environments[environment];
|
|
40
|
+
if (!configured) {
|
|
41
|
+
const known = Object.keys(environments);
|
|
42
|
+
throw new Error(`Unknown scenario environment '${environment}'. ` +
|
|
43
|
+
(known.length
|
|
44
|
+
? `Configured environments: ${known.join(', ')}`
|
|
45
|
+
: `Add scenarios.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
|
|
46
|
+
}
|
|
47
|
+
const resolved = { ...configured };
|
|
48
|
+
if (apiUrl !== undefined) {
|
|
49
|
+
const url = assertAbsoluteUrl('--api-url', apiUrl);
|
|
50
|
+
if (spawn && !LOCAL_HOSTNAMES.has(url.hostname)) {
|
|
51
|
+
throw new Error(`--spawn starts a server on this machine, but --api-url points at '${url.hostname}'. ` +
|
|
52
|
+
`Drop --spawn to run against an already-serving target, or point --api-url at localhost.`);
|
|
53
|
+
}
|
|
54
|
+
resolved.apiUrl = apiUrl;
|
|
55
|
+
}
|
|
56
|
+
if (appUrl !== undefined) {
|
|
57
|
+
assertAbsoluteUrl('--app-url', appUrl);
|
|
58
|
+
resolved.appUrl = appUrl;
|
|
59
|
+
}
|
|
60
|
+
return resolved;
|
|
61
|
+
};
|
|
@@ -10,6 +10,8 @@ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
|
|
|
10
10
|
spawn?: boolean;
|
|
11
11
|
keepAlive?: boolean;
|
|
12
12
|
trace?: boolean;
|
|
13
|
+
apiUrl?: string;
|
|
14
|
+
appUrl?: string;
|
|
13
15
|
}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
|
|
14
16
|
environment: string;
|
|
15
17
|
flows?: string;
|
|
@@ -21,6 +23,8 @@ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
|
|
|
21
23
|
spawn?: boolean;
|
|
22
24
|
keepAlive?: boolean;
|
|
23
25
|
trace?: boolean;
|
|
26
|
+
apiUrl?: string;
|
|
27
|
+
appUrl?: string;
|
|
24
28
|
}, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
|
|
25
29
|
environment: string;
|
|
26
30
|
flows?: string;
|
|
@@ -32,4 +36,6 @@ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
|
|
|
32
36
|
spawn?: boolean;
|
|
33
37
|
keepAlive?: boolean;
|
|
34
38
|
trace?: boolean;
|
|
39
|
+
apiUrl?: string;
|
|
40
|
+
appUrl?: string;
|
|
35
41
|
}, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|
|
@@ -12,6 +12,7 @@ import { resolveScenarioBrowserProvider, scenarioBrowserLifecycle, } from './sce
|
|
|
12
12
|
import { resolveScenarioActors } from '../../utils/resolve-scenario-actors.js';
|
|
13
13
|
import { spawnDevServer } from '../../server/spawn-dev-server.js';
|
|
14
14
|
import { buildScenarioPlan } from './scenario-plan.js';
|
|
15
|
+
import { resolveScenarioEnvironment } from './scenario-environment.js';
|
|
15
16
|
const isScenario = (wf) => wf?.scenario === true;
|
|
16
17
|
const listScenarios = (state) => Object.entries(state.workflows?.meta ?? {})
|
|
17
18
|
.filter(([, wf]) => isScenario(wf))
|
|
@@ -83,16 +84,20 @@ export const scenarioList = pikkuSessionlessFunc({
|
|
|
83
84
|
},
|
|
84
85
|
});
|
|
85
86
|
export const scenarioRun = pikkuSessionlessFunc({
|
|
86
|
-
func: async ({ logger, config, getInspectorState, variables }, { environment, flows, features, tags, excludeTags, coverage, browser = true, spawn = false, keepAlive = false, trace = false, }) => {
|
|
87
|
+
func: async ({ logger, config, getInspectorState, variables }, { environment, flows, features, tags, excludeTags, coverage, browser = true, spawn = false, keepAlive = false, trace = false, apiUrl, appUrl, }) => {
|
|
87
88
|
const state = await getInspectorState(true, false, false, true);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
// Resolved once, so actors, step env, the browser driver and any spawned
|
|
90
|
+
// server all target the same place — including when the target only exists
|
|
91
|
+
// at run time and arrives through --api-url/--app-url.
|
|
92
|
+
const env = resolveScenarioEnvironment({
|
|
93
|
+
environment,
|
|
94
|
+
environments: config.scenarios?.environments ?? {},
|
|
95
|
+
apiUrl,
|
|
96
|
+
appUrl,
|
|
97
|
+
spawn,
|
|
98
|
+
});
|
|
99
|
+
if (apiUrl || appUrl) {
|
|
100
|
+
logger.info(`Overriding '${environment}': apiUrl ${env.apiUrl}${env.appUrl ? `, appUrl ${env.appUrl}` : ''}`);
|
|
96
101
|
}
|
|
97
102
|
if (spawn) {
|
|
98
103
|
const { hostname, port } = new URL(env.apiUrl);
|