@pikku/inspector 0.12.102 → 0.12.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/add/add-cli.js +21 -11
- package/dist/add/add-define-screens.d.ts +8 -0
- package/dist/add/add-define-screens.js +140 -0
- package/dist/add/add-functions.js +8 -8
- package/dist/add/add-virtual-users.d.ts +13 -0
- package/dist/add/{add-personas.js → add-virtual-users.js} +33 -33
- package/dist/add/add-wire-addon.js +39 -2
- package/dist/add/add-workflow.js +14 -10
- package/dist/error-codes.d.ts +9 -3
- package/dist/error-codes.js +9 -3
- package/dist/index.d.ts +1 -1
- package/dist/inspector.js +7 -3
- package/dist/types.d.ts +45 -3
- package/dist/utils/extract-function-name.js +1 -1
- package/dist/utils/get-property-value.js +1 -1
- package/dist/utils/load-addon-functions-meta.d.ts +0 -5
- package/dist/utils/load-addon-functions-meta.js +53 -0
- package/dist/utils/post-process.d.ts +36 -34
- package/dist/utils/post-process.js +227 -169
- package/dist/utils/serialize-inspector-state.d.ts +7 -2
- package/dist/utils/serialize-inspector-state.js +10 -6
- package/dist/utils/single-declaration.d.ts +3 -3
- package/dist/utils/single-declaration.js +3 -3
- package/dist/utils/validate-single-step-workflows.d.ts +3 -0
- package/dist/utils/validate-single-step-workflows.js +32 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +3 -3
- package/dist/utils/workflow/dsl/patterns.d.ts +3 -3
- package/dist/utils/workflow/dsl/patterns.js +4 -4
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +6 -6
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -5
- package/dist/visit.js +4 -2
- package/knowledge/decisions/a-channel-with-auth-false-marks-its-connect-and-disconnect-sessionless.md +22 -0
- package/knowledge/decisions/a-declared-scope-id-never-shadows-a-definition.md +20 -0
- package/knowledge/decisions/a-dsl-step-virtual-user-is-the-literal-name-in-its-options.md +27 -0
- package/knowledge/decisions/a-feature-flags-anyof-is-read-from-the-ast-and-an-empty-list-is-refused.md +20 -0
- package/knowledge/decisions/a-non-literal-string-array-reads-as-absent.md +25 -0
- package/knowledge/decisions/a-permission-with-an-underscore-data-parameter-takes-no-data.md +20 -0
- package/knowledge/decisions/a-provider-login-virtual-user-is-never-runnable.md +27 -0
- package/knowledge/decisions/a-remote-addon-bound-credential-or-secret-must-be-wired.md +27 -0
- package/knowledge/decisions/a-route-auth-overrides-its-groups-auth.md +22 -0
- package/knowledge/decisions/a-route-requires-a-session-when-any-auth-signal-says-so.md +29 -0
- package/knowledge/decisions/a-webhook-source-with-verify-gets-a-singleton-secret-credential.md +21 -0
- package/knowledge/decisions/a-wire-with-auth-disabled-cannot-point-at-a-pikkufunc.md +21 -0
- package/knowledge/decisions/addon-contracts-cannot-wire-or-declare-handlers.md +25 -0
- package/knowledge/decisions/addon-declarations-name-the-secrets-and-credentials-the-app-lends-it.md +19 -0
- package/knowledge/decisions/an-inline-wiring-function-is-sessionless-unless-built-with-pikkufunc.md +23 -0
- package/knowledge/decisions/better-auth-admin-plugin-is-refused.md +24 -0
- package/knowledge/decisions/define-screens-scopes-must-be-string-literals.md +18 -0
- package/knowledge/decisions/index.md +38 -0
- package/knowledge/decisions/inspector-records-a-non-literal-global-grant-as-source-text.md +20 -0
- package/knowledge/decisions/only-one-pikku-better-auth-export-per-codebase.md +19 -0
- package/knowledge/decisions/scope-references-and-secret-aliases-are-critical-diagnostics.md +23 -0
- package/knowledge/decisions/scope-segments-may-not-be-wildcards-or-contain-colons.md +23 -0
- package/knowledge/decisions/shorthand-permission-entries-are-extracted.md +22 -0
- package/knowledge/decisions/system-roles-declare-literal-unqualified-scopes.md +28 -0
- package/knowledge/decisions/the-inspector-records-agent-permissions-only-to-aggregate-services.md +14 -0
- package/knowledge/decisions/vault-secrets-reach-no-sink-and-pii-stays-out-of-logs-and-webhooks.md +30 -0
- package/knowledge/decisions/webhook-gateway-wrappers-are-sessionless-and-gateway-auth-runs-inside-the-handler.md +26 -0
- package/knowledge/index.md +14 -0
- package/knowledge/questions/does-a-non-literal-remote-addon-auth-get-checked.md +26 -0
- package/knowledge/questions/index.md +16 -0
- package/knowledge/questions/is-an-incomplete-oauth2-block-dropped-without-a-log.md +23 -0
- package/knowledge/questions/should-app-functions-named-like-scenario-instrumentation-be-refused.md +28 -0
- package/knowledge/questions/should-the-openapi-document-default-every-operation-to-a-bearer-or-api-key.md +22 -0
- package/package.json +3 -3
- package/dist/add/add-personas.d.ts +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
+
## 0.12.106
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- fdcc821: Fabric is gone from the open toolchain. The previous names are removed with no aliases.
|
|
6
|
+
|
|
7
|
+
The CLI group is `pikku cloud …` and the export is `@pikku/cli/cloud`. The project link is the `cloud` key of `pikku.config.json` (`cloud.projectId`), the CLI keeps its local state in `~/.pikkustudio` (run `pikku cloud login` again), and the project config file is `pikkustudio.config.json`.
|
|
8
|
+
|
|
9
|
+
Every environment variable drops the `FABRIC_` prefix for `PIKKUSTUDIO_` with the same suffix: `PIKKUSTUDIO_API_URL`, `PIKKUSTUDIO_PROJECT_ID`, `PIKKUSTUDIO_TOKEN`, `PIKKUSTUDIO_OPERATOR_TOKEN`, `PIKKUSTUDIO_AUTH_PUBLIC_KEY`, `PIKKUSTUDIO_STAGE_ID` and the rest. Rename them wherever you set them.
|
|
10
|
+
|
|
11
|
+
Wire ids: the operator sign-in route is `/sign-in/cloud`, the plugin and mode literal is `'cloud'`, the Better Auth plugin is `pikkuCloud`, its session field is `pikkustudioadmin`, and the signature header is `x-cloud-sig`. The cloud API calls are `getCloudLogs`, `getCloudLogsByStageKind`, `cloudCliProjects`, `cloudCliDeployments`, `providedByCloud` and `recordCloudAnalyticsEvents`. Addon packages are `@pikkustudio/*`, scaffolded files live under `scaffold/cloud` and `__cloud_*__`, and the API hosts are the `pikkustudio.app` and `pikkustudio.dev` domains.
|
|
12
|
+
|
|
13
|
+
`pikku skills install --fabric` is `--cloud`, the install group is `cloud`, and the skills are `pikku-cloud`, `pikku-cloud-debug`, `pikku-admin-to-cloud` and `pikku-blueprint-to-cloud`.
|
|
14
|
+
|
|
15
|
+
The stale analytics forwarder copies are removed from the online-shop example and the starter template.
|
|
16
|
+
|
|
17
|
+
The sealed-box salt stays `pikkufabric/sealed-box/v1`: it is a cryptographic constant that already-sealed secrets depend on.
|
|
18
|
+
|
|
19
|
+
- 33b102d: Virtual users now have one name everywhere. The id column of `virtual_user_run` and `virtual_user_schedule` is `virtual_user_id` (a migration renames it in place), the CLI group is `pikku virtual-user …`, and the declarations and imports live under `@pikku/core/virtual-user`. The previous names are removed with no aliases.
|
|
20
|
+
|
|
21
|
+
The default virtual user email domain is now `virtualuser.invalid`: apps that rely on the default domain get new accounts at the next provisioning (`pikku virtual-user sync`, deploy, or `pikku dev` start), and the old rows stay unused.
|
|
22
|
+
|
|
23
|
+
The sign-in surface carries the same name. The route is `/sign-in/virtual-user`, the environment variables are `PIKKU_ALLOW_VIRTUAL_USER_SIGN_IN` and the `PIKKU_DEV_VIRTUAL_USER_SIGN_IN` / `VITE_*` equivalents, the Better Auth plugin is `pikkuVirtualUser`, the scenario step option and wire member are `virtualUser` and `virtualUsers`, and the Better Auth user flag column is `virtual_user`. Rename the environment variables wherever you set them, and add a migration that renames the flag column on the `user` table. Nothing is aliased.
|
|
24
|
+
|
|
25
|
+
The conversation runner is now `@pikku/core/virtual-user/conversation`, replacing the old `-flow` subpath. Update the import; the old subpath is gone.
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [fdcc821]
|
|
28
|
+
- Updated dependencies [33b102d]
|
|
29
|
+
- Updated dependencies [6a25c70]
|
|
30
|
+
- Updated dependencies [a084c3b]
|
|
31
|
+
- @pikku/core@0.12.142
|
|
32
|
+
|
|
33
|
+
## 0.12.105
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- b04e444: Fix CLI commands that come from an addon package. Auth and permissions set on such a command are now merged into the addon's own function instead of replacing it, and the command's input is shaped by the addon's own schema (for example a comma list becomes an array). The inspector now also resolves `...refCLI('addon:contract')`, spreads and `defineCLICommands` identifiers inside a command's `subcommands`.
|
|
38
|
+
- Updated dependencies [b04e444]
|
|
39
|
+
- Updated dependencies [d3d8a98]
|
|
40
|
+
- Updated dependencies [658da50]
|
|
41
|
+
- @pikku/core@0.12.141
|
|
42
|
+
|
|
43
|
+
## 0.12.104
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- 5464b86: Add the `pikku verify` command over `@pikku/code-edit/verify`: codegen, the backend and frontend type-checks and static correctness checks as structured findings with fix hints. The inspector warns about single-step workflows (PKU644) and PKU111 now names the file and line.
|
|
48
|
+
- Updated dependencies [f396538]
|
|
49
|
+
- @pikku/core@0.12.140
|
|
50
|
+
|
|
51
|
+
## 0.12.103
|
|
52
|
+
|
|
53
|
+
### Patch Changes
|
|
54
|
+
|
|
55
|
+
- 53ac40d: An addon can ship screens: `wireAddon({ ui: true })` mounts what the package declares with `defineScreens`, the inspector reads them, `pikku all` emits the screens manifest and the installed-addons registry, and `x-pikku-addon` narrows a session to the addon's role. `pikku new addon --ui` scaffolds one; its tables must be prefixed `<name>_`.
|
|
56
|
+
- Updated dependencies [a196a21]
|
|
57
|
+
- Updated dependencies [53ac40d]
|
|
58
|
+
- Updated dependencies [443fd40]
|
|
59
|
+
- Updated dependencies [56c8047]
|
|
60
|
+
- @pikku/core@0.12.139
|
|
61
|
+
|
|
1
62
|
## 0.12.102
|
|
2
63
|
|
|
3
64
|
### Patch Changes
|
package/dist/add/add-cli.js
CHANGED
|
@@ -325,15 +325,23 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
|
|
|
325
325
|
}
|
|
326
326
|
break;
|
|
327
327
|
case 'subcommands': {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
328
|
+
const subcommands = {};
|
|
329
|
+
const collect = (node) => {
|
|
330
|
+
const refContract = resolveRefContract(node, 'refCLI', inspectorState.exportedContracts.addonCli);
|
|
331
|
+
if (refContract) {
|
|
332
|
+
Object.assign(subcommands, refContract.contract);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const target = ts.isIdentifier(node)
|
|
336
|
+
? resolveIdentifier(node, typeChecker, ['defineCLICommands'])
|
|
337
|
+
: node;
|
|
338
|
+
if (!target || !ts.isObjectLiteralExpression(target))
|
|
339
|
+
return;
|
|
340
|
+
for (const subProp of target.properties) {
|
|
341
|
+
if (ts.isSpreadAssignment(subProp)) {
|
|
342
|
+
collect(subProp.expression);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
337
345
|
if (!ts.isPropertyAssignment(subProp))
|
|
338
346
|
continue;
|
|
339
347
|
const subName = getPropertyName(subProp);
|
|
@@ -341,10 +349,12 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
|
|
|
341
349
|
continue;
|
|
342
350
|
const subCommand = processCommand(logger, inspectorState, options, subName, subProp.initializer, sourceFile, typeChecker, programName, fullPath, programTags);
|
|
343
351
|
if (subCommand) {
|
|
344
|
-
|
|
352
|
+
subcommands[subName] = subCommand;
|
|
345
353
|
}
|
|
346
354
|
}
|
|
347
|
-
}
|
|
355
|
+
};
|
|
356
|
+
collect(prop.initializer);
|
|
357
|
+
meta.subcommands = subcommands;
|
|
348
358
|
break;
|
|
349
359
|
}
|
|
350
360
|
case 'isDefault':
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import type { InspectorState, InspectorLogger } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect `defineScreens({ title, icon, screens })` and record the manifest.
|
|
5
|
+
* A screen with a field that is not a literal is reported and left out: a
|
|
6
|
+
* navigation entry the host cannot read would otherwise vanish with no sign.
|
|
7
|
+
*/
|
|
8
|
+
export declare function addDefineScreens(node: ts.Node, state: InspectorState, logger: InspectorLogger): void;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { ErrorCode } from '../error-codes.js';
|
|
3
|
+
const propName = (prop) => {
|
|
4
|
+
if (!ts.isPropertyAssignment(prop))
|
|
5
|
+
return undefined;
|
|
6
|
+
return ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
|
|
7
|
+
? prop.name.text
|
|
8
|
+
: undefined;
|
|
9
|
+
};
|
|
10
|
+
const stringArray = (node) => {
|
|
11
|
+
if (!ts.isArrayLiteralExpression(node))
|
|
12
|
+
return undefined;
|
|
13
|
+
const values = [];
|
|
14
|
+
for (const element of node.elements) {
|
|
15
|
+
if (!ts.isStringLiteral(element))
|
|
16
|
+
return undefined;
|
|
17
|
+
values.push(element.text);
|
|
18
|
+
}
|
|
19
|
+
return values;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The module specifier of `() => import('./screens/x')`. Anything else cannot
|
|
23
|
+
* be handed to the host's bundler as a lazy import, so it is not read.
|
|
24
|
+
*/
|
|
25
|
+
const lazyImportSpecifier = (node) => {
|
|
26
|
+
if (!ts.isArrowFunction(node))
|
|
27
|
+
return undefined;
|
|
28
|
+
const body = node.body;
|
|
29
|
+
if (!ts.isCallExpression(body) ||
|
|
30
|
+
body.expression.kind !== ts.SyntaxKind.ImportKeyword)
|
|
31
|
+
return undefined;
|
|
32
|
+
const [specifier] = body.arguments;
|
|
33
|
+
return specifier && ts.isStringLiteral(specifier) ? specifier.text : undefined;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Detect `defineScreens({ title, icon, screens })` and record the manifest.
|
|
37
|
+
* A screen with a field that is not a literal is reported and left out: a
|
|
38
|
+
* navigation entry the host cannot read would otherwise vanish with no sign.
|
|
39
|
+
*/
|
|
40
|
+
export function addDefineScreens(node, state, logger) {
|
|
41
|
+
if (!ts.isCallExpression(node))
|
|
42
|
+
return;
|
|
43
|
+
const { expression, arguments: args } = node;
|
|
44
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'defineScreens')
|
|
45
|
+
return;
|
|
46
|
+
const [firstArg] = args;
|
|
47
|
+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
|
|
48
|
+
return;
|
|
49
|
+
const fail = (message) => logger.critical(ErrorCode.ADDON_SCREENS_NOT_STATIC, message);
|
|
50
|
+
let title;
|
|
51
|
+
let icon;
|
|
52
|
+
const screens = [];
|
|
53
|
+
for (const prop of firstArg.properties) {
|
|
54
|
+
const key = propName(prop);
|
|
55
|
+
if (!key || !ts.isPropertyAssignment(prop))
|
|
56
|
+
continue;
|
|
57
|
+
if (key === 'title' && ts.isStringLiteral(prop.initializer)) {
|
|
58
|
+
title = prop.initializer.text;
|
|
59
|
+
}
|
|
60
|
+
else if (key === 'icon' && ts.isStringLiteral(prop.initializer)) {
|
|
61
|
+
icon = prop.initializer.text;
|
|
62
|
+
}
|
|
63
|
+
else if (key === 'screens') {
|
|
64
|
+
if (!ts.isArrayLiteralExpression(prop.initializer)) {
|
|
65
|
+
fail(`defineScreens's screens must be an array literal, got: ${prop.initializer.getText()}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const element of prop.initializer.elements) {
|
|
69
|
+
if (!ts.isObjectLiteralExpression(element)) {
|
|
70
|
+
fail(`A defineScreens screen must be an object literal, got: ${element.getText()}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
let path;
|
|
74
|
+
let screenTitle;
|
|
75
|
+
let nav;
|
|
76
|
+
let scopes;
|
|
77
|
+
let component;
|
|
78
|
+
let app;
|
|
79
|
+
for (const field of element.properties) {
|
|
80
|
+
const fieldKey = propName(field);
|
|
81
|
+
if (!fieldKey || !ts.isPropertyAssignment(field))
|
|
82
|
+
continue;
|
|
83
|
+
const value = field.initializer;
|
|
84
|
+
if (fieldKey === 'path' && ts.isStringLiteral(value)) {
|
|
85
|
+
path = value.text;
|
|
86
|
+
}
|
|
87
|
+
else if (fieldKey === 'title' && ts.isStringLiteral(value)) {
|
|
88
|
+
screenTitle = value.text;
|
|
89
|
+
}
|
|
90
|
+
else if (fieldKey === 'nav' &&
|
|
91
|
+
(value.kind === ts.SyntaxKind.TrueKeyword ||
|
|
92
|
+
value.kind === ts.SyntaxKind.FalseKeyword)) {
|
|
93
|
+
nav = value.kind === ts.SyntaxKind.TrueKeyword;
|
|
94
|
+
}
|
|
95
|
+
else if (fieldKey === 'scopes') {
|
|
96
|
+
scopes = stringArray(value);
|
|
97
|
+
if (!scopes) {
|
|
98
|
+
fail(`A screen's scopes must be an array of string literals, got: ${value.getText()}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (fieldKey === 'component') {
|
|
102
|
+
component = lazyImportSpecifier(value);
|
|
103
|
+
if (!component) {
|
|
104
|
+
fail(`A screen's component must be () => import('<literal>'), got: ${value.getText()}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (fieldKey === 'app' && ts.isStringLiteral(value)) {
|
|
108
|
+
app = value.text;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!path || !screenTitle || (!component && !app)) {
|
|
112
|
+
fail(`A defineScreens screen needs a literal path and title, and either a lazy component or an app: ${element.getText()}`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (component && app) {
|
|
116
|
+
fail(`Screen '${path}' sets both component and app; a screen is one or the other`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
screens.push({
|
|
120
|
+
path,
|
|
121
|
+
title: screenTitle,
|
|
122
|
+
...(nav !== undefined ? { nav } : {}),
|
|
123
|
+
...(scopes ? { scopes } : {}),
|
|
124
|
+
...(component ? { component } : { app: app }),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!title) {
|
|
130
|
+
fail(`defineScreens needs a literal title`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
logger.debug(`• Found defineScreens: ${title} (${screens.length} screens)`);
|
|
134
|
+
state.screensManifest = {
|
|
135
|
+
title,
|
|
136
|
+
...(icon ? { icon } : {}),
|
|
137
|
+
file: node.getSourceFile().fileName,
|
|
138
|
+
screens,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -21,7 +21,7 @@ const SCENARIO_SURFACE_KEYS = [
|
|
|
21
21
|
* copied into an addon package by a consumer, a called identifier cannot.
|
|
22
22
|
*/
|
|
23
23
|
const SCENARIO_STEP_KINDS = new Map([
|
|
24
|
-
['pikkuScenarioStep', '
|
|
24
|
+
['pikkuScenarioStep', 'virtualUser'],
|
|
25
25
|
['pikkuPlatformScenarioStep', 'platform'],
|
|
26
26
|
['pikkuAddonScenarioStep', 'addon'],
|
|
27
27
|
]);
|
|
@@ -424,7 +424,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
424
424
|
let workflowRetries;
|
|
425
425
|
let workflowTimeout;
|
|
426
426
|
let scenarioStepSurfaces;
|
|
427
|
-
let
|
|
427
|
+
let scenarioStepRequiresVirtualUser;
|
|
428
428
|
let scenarioStepAddon;
|
|
429
429
|
let scenarioStepTemplate;
|
|
430
430
|
let scopes;
|
|
@@ -560,9 +560,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
560
560
|
? declaredSurfaces
|
|
561
561
|
: undefined;
|
|
562
562
|
// A browser binding cannot be provisioned without a window to open, so it
|
|
563
|
-
// requires
|
|
564
|
-
|
|
565
|
-
getPropertyValue(firstArg, '
|
|
563
|
+
// requires a virtual user whether or not the step said so.
|
|
564
|
+
scenarioStepRequiresVirtualUser =
|
|
565
|
+
getPropertyValue(firstArg, 'virtualUser') === true ||
|
|
566
566
|
declaredSurfaces.includes('browser')
|
|
567
567
|
? true
|
|
568
568
|
: undefined;
|
|
@@ -1083,10 +1083,10 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
1083
1083
|
workflowTimeout: workflowTimeout ?? undefined,
|
|
1084
1084
|
compensate: compensateInitializer ? true : undefined,
|
|
1085
1085
|
scenarioStepSurfaces,
|
|
1086
|
-
|
|
1087
|
-
// `
|
|
1086
|
+
scenarioStepRequiresVirtualUser,
|
|
1087
|
+
// `virtualUser` is the default reading of an unmarked step, so it is left off
|
|
1088
1088
|
// rather than written into every step meta a project already has.
|
|
1089
|
-
scenarioStepKind: scenarioStepKind && scenarioStepKind !== '
|
|
1089
|
+
scenarioStepKind: scenarioStepKind && scenarioStepKind !== 'virtualUser'
|
|
1090
1090
|
? scenarioStepKind
|
|
1091
1091
|
: undefined,
|
|
1092
1092
|
scenarioStepAddon,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AddWiring } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Inspector for `defineVirtualUsers()` calls.
|
|
4
|
+
*
|
|
5
|
+
* A virtual user is entirely declaration — a person, their roles, what they want —
|
|
6
|
+
* so there is no code body and nothing to resolve at runtime. What is read here
|
|
7
|
+
* is the whole thing, and it is generated straight to `virtual-users.gen.json` for
|
|
8
|
+
* the CLI and the console to read without loading the app.
|
|
9
|
+
*
|
|
10
|
+
* `name` is the one field that cannot be defaulted. A virtual user without one is
|
|
11
|
+
* not a person, and the console has nothing to show.
|
|
12
|
+
*/
|
|
13
|
+
export declare const addVirtualUsers: AddWiring;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { DISPOSITIONS } from '@pikku/core/virtual-user';
|
|
3
|
-
import {
|
|
3
|
+
import { isRunnableVirtualUser } from '@pikku/core/virtual-user';
|
|
4
4
|
import { ErrorCode } from '../error-codes.js';
|
|
5
5
|
import { claimSingleDeclaration } from '../utils/single-declaration.js';
|
|
6
6
|
import { booleanProperty, getProperty, numberProperty, stringArrayProperty, stringProperty, unwrapAs, } from '../utils/literal-properties.js';
|
|
@@ -15,7 +15,7 @@ const RATES = [
|
|
|
15
15
|
*
|
|
16
16
|
* The ranges are checked here rather than at run time because these are the
|
|
17
17
|
* kind of typo that does not throw: `repeatRate: 18` meaning 18% would silently
|
|
18
|
-
* double every call the
|
|
18
|
+
* double every call the virtual user makes, and you would read the run as a product
|
|
19
19
|
* bug.
|
|
20
20
|
*/
|
|
21
21
|
const tuningProperty = (config, onError) => {
|
|
@@ -72,7 +72,7 @@ const accountProperty = (value) => {
|
|
|
72
72
|
const provider = stringProperty(value, 'provider');
|
|
73
73
|
return provider === undefined ? {} : { provider };
|
|
74
74
|
};
|
|
75
|
-
const linkedAccountsProperty = (config,
|
|
75
|
+
const linkedAccountsProperty = (config, virtualUserId, logger) => {
|
|
76
76
|
const value = getProperty(config, 'linkedAccounts');
|
|
77
77
|
if (!value || !ts.isObjectLiteralExpression(value)) {
|
|
78
78
|
return undefined;
|
|
@@ -83,12 +83,12 @@ const linkedAccountsProperty = (config, personaId, logger) => {
|
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
85
|
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
|
|
86
|
-
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, `
|
|
86
|
+
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, `VirtualUser '${virtualUserId}' has a linked account whose key is not a literal.`);
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
89
89
|
const account = accountProperty(prop.initializer);
|
|
90
90
|
if (!account) {
|
|
91
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
91
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${virtualUserId}' linked account '${prop.name.text}' must be an object literal.`);
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
94
94
|
accounts[prop.name.text] = account;
|
|
@@ -96,22 +96,22 @@ const linkedAccountsProperty = (config, personaId, logger) => {
|
|
|
96
96
|
return Object.keys(accounts).length ? accounts : undefined;
|
|
97
97
|
};
|
|
98
98
|
/**
|
|
99
|
-
* Inspector for `
|
|
99
|
+
* Inspector for `defineVirtualUsers()` calls.
|
|
100
100
|
*
|
|
101
|
-
* A
|
|
101
|
+
* A virtual user is entirely declaration — a person, their roles, what they want —
|
|
102
102
|
* so there is no code body and nothing to resolve at runtime. What is read here
|
|
103
|
-
* is the whole thing, and it is generated straight to `
|
|
103
|
+
* is the whole thing, and it is generated straight to `virtual-users.gen.json` for
|
|
104
104
|
* the CLI and the console to read without loading the app.
|
|
105
105
|
*
|
|
106
|
-
* `name` is the one field that cannot be defaulted. A
|
|
106
|
+
* `name` is the one field that cannot be defaulted. A virtual user without one is
|
|
107
107
|
* not a person, and the console has nothing to show.
|
|
108
108
|
*/
|
|
109
|
-
export const
|
|
109
|
+
export const addVirtualUsers = (logger, node, _checker, state) => {
|
|
110
110
|
if (!ts.isCallExpression(node)) {
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
const expression = node.expression;
|
|
114
|
-
if (!ts.isIdentifier(expression) || expression.text !== '
|
|
114
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'defineVirtualUsers') {
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
const firstArg = node.arguments[0];
|
|
@@ -120,11 +120,11 @@ export const addPersonas = (logger, node, _checker, state) => {
|
|
|
120
120
|
}
|
|
121
121
|
const unwrapped = unwrapAs(firstArg);
|
|
122
122
|
if (!ts.isObjectLiteralExpression(unwrapped)) {
|
|
123
|
-
logger.critical(ErrorCode.INVALID_VALUE, '
|
|
123
|
+
logger.critical(ErrorCode.INVALID_VALUE, 'defineVirtualUsers() needs an inline object — virtual users are read statically, never evaluated.');
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
const sourceFile = node.getSourceFile().fileName;
|
|
127
|
-
if (!claimSingleDeclaration(logger, state.
|
|
127
|
+
if (!claimSingleDeclaration(logger, state.virtualUsers.files, ErrorCode.DUPLICATE_VIRTUAL_USERS_DEFINITION, 'defineVirtualUsers', sourceFile)) {
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
const known = Object.keys(DISPOSITIONS);
|
|
@@ -133,46 +133,46 @@ export const addPersonas = (logger, node, _checker, state) => {
|
|
|
133
133
|
continue;
|
|
134
134
|
}
|
|
135
135
|
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
|
|
136
|
-
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'A
|
|
136
|
+
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'A virtual user is declared with a key that is not a literal.');
|
|
137
137
|
continue;
|
|
138
138
|
}
|
|
139
139
|
const id = prop.name.text;
|
|
140
140
|
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
141
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
141
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${id}' must be an object literal.`);
|
|
142
142
|
continue;
|
|
143
143
|
}
|
|
144
144
|
const config = prop.initializer;
|
|
145
145
|
const name = stringProperty(config, 'name');
|
|
146
146
|
if (!name) {
|
|
147
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
147
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${id}' needs a literal 'name' — a virtual user is a person, and the console has nothing to show without one.`);
|
|
148
148
|
continue;
|
|
149
149
|
}
|
|
150
|
-
// The generated `
|
|
150
|
+
// The generated `defineVirtualUsers` types this as a union, so a bad value only
|
|
151
151
|
// reaches here in source that does not compile. Caught anyway: a
|
|
152
152
|
// disposition is the entire behaviour, and defaulting a typo to `realistic`
|
|
153
153
|
// would run a different person than the one that was written.
|
|
154
154
|
const disposition = stringProperty(config, 'disposition');
|
|
155
155
|
if (disposition !== undefined && !known.includes(disposition)) {
|
|
156
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
156
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${id}' has an unknown disposition '${disposition}'. One of: ${known.join(', ')}`);
|
|
157
157
|
continue;
|
|
158
158
|
}
|
|
159
159
|
let tuningFailed = false;
|
|
160
160
|
const tuning = tuningProperty(config, (message) => {
|
|
161
161
|
tuningFailed = true;
|
|
162
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
162
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${id}': ${message}`);
|
|
163
163
|
});
|
|
164
164
|
if (tuningFailed) {
|
|
165
165
|
continue;
|
|
166
166
|
}
|
|
167
167
|
const account = accountProperty(getProperty(config, 'account'));
|
|
168
168
|
const declaredRunnable = booleanProperty(config, 'runnable');
|
|
169
|
-
const
|
|
169
|
+
const virtualUser = {
|
|
170
170
|
id,
|
|
171
171
|
name,
|
|
172
172
|
roles: stringArrayProperty(config, 'roles') ?? [],
|
|
173
173
|
goals: stringArrayProperty(config, 'goals') ?? [],
|
|
174
174
|
tags: stringArrayProperty(config, 'tags') ?? [],
|
|
175
|
-
runnable:
|
|
175
|
+
runnable: isRunnableVirtualUser({
|
|
176
176
|
...(declaredRunnable !== undefined
|
|
177
177
|
? { runnable: declaredRunnable }
|
|
178
178
|
: {}),
|
|
@@ -182,38 +182,38 @@ export const addPersonas = (logger, node, _checker, state) => {
|
|
|
182
182
|
};
|
|
183
183
|
const jobTitle = stringProperty(config, 'jobTitle');
|
|
184
184
|
if (jobTitle !== undefined)
|
|
185
|
-
|
|
185
|
+
virtualUser.jobTitle = jobTitle;
|
|
186
186
|
const description = stringProperty(config, 'description');
|
|
187
187
|
if (description !== undefined)
|
|
188
|
-
|
|
188
|
+
virtualUser.description = description;
|
|
189
189
|
const avatarUrl = stringProperty(config, 'avatarUrl');
|
|
190
190
|
if (avatarUrl !== undefined)
|
|
191
|
-
|
|
191
|
+
virtualUser.avatarUrl = avatarUrl;
|
|
192
192
|
const personality = stringProperty(config, 'personality');
|
|
193
193
|
if (personality !== undefined)
|
|
194
|
-
|
|
194
|
+
virtualUser.personality = personality;
|
|
195
195
|
const app = stringProperty(config, 'app');
|
|
196
196
|
if (app !== undefined)
|
|
197
|
-
|
|
197
|
+
virtualUser.app = app;
|
|
198
198
|
if (disposition !== undefined) {
|
|
199
|
-
|
|
199
|
+
virtualUser.disposition = disposition;
|
|
200
200
|
}
|
|
201
201
|
if (tuning !== undefined)
|
|
202
|
-
|
|
202
|
+
virtualUser.tuning = tuning;
|
|
203
203
|
const fixtures = stringArrayProperty(config, 'fixtures');
|
|
204
204
|
if (fixtures !== undefined)
|
|
205
|
-
|
|
205
|
+
virtualUser.fixtures = fixtures;
|
|
206
206
|
// Left absent rather than resolved here: "everywhere but production" is a
|
|
207
207
|
// fact about the config, and the inspector has no business baking today's
|
|
208
208
|
// environment list into a meta file that outlives it.
|
|
209
209
|
const environments = stringArrayProperty(config, 'environments');
|
|
210
210
|
if (environments !== undefined)
|
|
211
|
-
|
|
211
|
+
virtualUser.environments = environments;
|
|
212
212
|
if (account !== undefined)
|
|
213
|
-
|
|
213
|
+
virtualUser.account = account;
|
|
214
214
|
const linkedAccounts = linkedAccountsProperty(config, id, logger);
|
|
215
215
|
if (linkedAccounts !== undefined)
|
|
216
|
-
|
|
217
|
-
state.
|
|
216
|
+
virtualUser.linkedAccounts = linkedAccounts;
|
|
217
|
+
state.virtualUsers.definitions.push(virtualUser);
|
|
218
218
|
}
|
|
219
219
|
};
|
|
@@ -87,7 +87,8 @@ export function addWireAddon(node, state, logger) {
|
|
|
87
87
|
if (!ts.isCallExpression(node))
|
|
88
88
|
return;
|
|
89
89
|
const { expression, arguments: args } = node;
|
|
90
|
-
if (!ts.isIdentifier(expression) ||
|
|
90
|
+
if (!ts.isIdentifier(expression) ||
|
|
91
|
+
expression.text !== 'wireAddon')
|
|
91
92
|
return;
|
|
92
93
|
const [firstArg] = args;
|
|
93
94
|
if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
|
|
@@ -99,8 +100,10 @@ export function addWireAddon(node, state, logger) {
|
|
|
99
100
|
let expose;
|
|
100
101
|
let mcpEndpoint;
|
|
101
102
|
let auth;
|
|
103
|
+
let ui;
|
|
102
104
|
let tags;
|
|
103
105
|
let scopes;
|
|
106
|
+
let uses;
|
|
104
107
|
let secretOverrides;
|
|
105
108
|
let variableOverrides;
|
|
106
109
|
let credentialOverrides;
|
|
@@ -108,6 +111,9 @@ export function addWireAddon(node, state, logger) {
|
|
|
108
111
|
let credentialGrants;
|
|
109
112
|
let globalSecrets;
|
|
110
113
|
let globalCredentials;
|
|
114
|
+
let contentBucket;
|
|
115
|
+
let contentGrants;
|
|
116
|
+
let globalContent;
|
|
111
117
|
for (const prop of firstArg.properties) {
|
|
112
118
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
|
|
113
119
|
continue;
|
|
@@ -154,12 +160,25 @@ export function addWireAddon(node, state, logger) {
|
|
|
154
160
|
prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
|
|
155
161
|
auth = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
156
162
|
}
|
|
163
|
+
else if (key === 'ui' &&
|
|
164
|
+
(prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
165
|
+
prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
|
|
166
|
+
ui = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
167
|
+
}
|
|
157
168
|
else if (key === 'tags') {
|
|
158
169
|
tags = parseStringArray(prop.initializer);
|
|
159
170
|
}
|
|
160
171
|
else if (key === 'scopes') {
|
|
161
172
|
scopes = parseStringArray(prop.initializer);
|
|
162
173
|
}
|
|
174
|
+
else if (key === 'uses') {
|
|
175
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
176
|
+
uses = parseStringRecord(prop.initializer);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
logger.critical(ErrorCode.ADDON_EXPOSE_NOT_STATIC, `wireAddon's uses must be an object literal, got: ${prop.initializer.getText()}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
163
182
|
else if (key === 'secretOverrides' &&
|
|
164
183
|
ts.isObjectLiteralExpression(prop.initializer)) {
|
|
165
184
|
secretOverrides = parseStringRecord(prop.initializer);
|
|
@@ -190,12 +209,26 @@ export function addWireAddon(node, state, logger) {
|
|
|
190
209
|
? prop.initializer.text
|
|
191
210
|
: prop.initializer.getText();
|
|
192
211
|
}
|
|
212
|
+
else if (key === 'contentBucket' &&
|
|
213
|
+
ts.isStringLiteral(prop.initializer)) {
|
|
214
|
+
contentBucket = prop.initializer.text;
|
|
215
|
+
}
|
|
216
|
+
else if (key === 'contentGrants' &&
|
|
217
|
+
ts.isObjectLiteralExpression(prop.initializer)) {
|
|
218
|
+
contentGrants = parseStringRecord(prop.initializer);
|
|
219
|
+
}
|
|
220
|
+
else if (key === 'globalContent') {
|
|
221
|
+
globalContent = ts.isStringLiteral(prop.initializer)
|
|
222
|
+
? prop.initializer.text
|
|
223
|
+
: prop.initializer.getText();
|
|
224
|
+
}
|
|
193
225
|
}
|
|
194
226
|
if (!name || !pkg)
|
|
195
227
|
return;
|
|
196
|
-
logger.debug(`• Found
|
|
228
|
+
logger.debug(`• Found ${expression.text}: ${name} → ${pkg}`);
|
|
197
229
|
state.rpc.wireAddonDeclarations.set(name, {
|
|
198
230
|
package: pkg,
|
|
231
|
+
...(ui ? { ui } : {}),
|
|
199
232
|
file: node.getSourceFile().fileName,
|
|
200
233
|
rpcEndpoint,
|
|
201
234
|
mcp,
|
|
@@ -204,6 +237,7 @@ export function addWireAddon(node, state, logger) {
|
|
|
204
237
|
auth,
|
|
205
238
|
tags,
|
|
206
239
|
scopes,
|
|
240
|
+
uses,
|
|
207
241
|
secretOverrides,
|
|
208
242
|
variableOverrides,
|
|
209
243
|
credentialOverrides,
|
|
@@ -211,6 +245,9 @@ export function addWireAddon(node, state, logger) {
|
|
|
211
245
|
credentialGrants,
|
|
212
246
|
globalSecrets,
|
|
213
247
|
globalCredentials,
|
|
248
|
+
contentBucket,
|
|
249
|
+
contentGrants,
|
|
250
|
+
globalContent,
|
|
214
251
|
});
|
|
215
252
|
state.rpc.usedAddons.add(name);
|
|
216
253
|
state.rpc.wireAddonFiles.add(node.getSourceFile().fileName);
|