@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/add/add-cli.js +21 -11
  3. package/dist/add/add-define-screens.d.ts +8 -0
  4. package/dist/add/add-define-screens.js +140 -0
  5. package/dist/add/add-functions.js +8 -8
  6. package/dist/add/add-virtual-users.d.ts +13 -0
  7. package/dist/add/{add-personas.js → add-virtual-users.js} +33 -33
  8. package/dist/add/add-wire-addon.js +39 -2
  9. package/dist/add/add-workflow.js +14 -10
  10. package/dist/error-codes.d.ts +9 -3
  11. package/dist/error-codes.js +9 -3
  12. package/dist/index.d.ts +1 -1
  13. package/dist/inspector.js +7 -3
  14. package/dist/types.d.ts +45 -3
  15. package/dist/utils/extract-function-name.js +1 -1
  16. package/dist/utils/get-property-value.js +1 -1
  17. package/dist/utils/load-addon-functions-meta.d.ts +0 -5
  18. package/dist/utils/load-addon-functions-meta.js +53 -0
  19. package/dist/utils/post-process.d.ts +36 -34
  20. package/dist/utils/post-process.js +227 -169
  21. package/dist/utils/serialize-inspector-state.d.ts +7 -2
  22. package/dist/utils/serialize-inspector-state.js +10 -6
  23. package/dist/utils/single-declaration.d.ts +3 -3
  24. package/dist/utils/single-declaration.js +3 -3
  25. package/dist/utils/validate-single-step-workflows.d.ts +3 -0
  26. package/dist/utils/validate-single-step-workflows.js +32 -0
  27. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +3 -3
  28. package/dist/utils/workflow/dsl/patterns.d.ts +3 -3
  29. package/dist/utils/workflow/dsl/patterns.js +4 -4
  30. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +6 -6
  31. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -5
  32. package/dist/visit.js +4 -2
  33. package/knowledge/decisions/a-channel-with-auth-false-marks-its-connect-and-disconnect-sessionless.md +22 -0
  34. package/knowledge/decisions/a-declared-scope-id-never-shadows-a-definition.md +20 -0
  35. package/knowledge/decisions/a-dsl-step-virtual-user-is-the-literal-name-in-its-options.md +27 -0
  36. package/knowledge/decisions/a-feature-flags-anyof-is-read-from-the-ast-and-an-empty-list-is-refused.md +20 -0
  37. package/knowledge/decisions/a-non-literal-string-array-reads-as-absent.md +25 -0
  38. package/knowledge/decisions/a-permission-with-an-underscore-data-parameter-takes-no-data.md +20 -0
  39. package/knowledge/decisions/a-provider-login-virtual-user-is-never-runnable.md +27 -0
  40. package/knowledge/decisions/a-remote-addon-bound-credential-or-secret-must-be-wired.md +27 -0
  41. package/knowledge/decisions/a-route-auth-overrides-its-groups-auth.md +22 -0
  42. package/knowledge/decisions/a-route-requires-a-session-when-any-auth-signal-says-so.md +29 -0
  43. package/knowledge/decisions/a-webhook-source-with-verify-gets-a-singleton-secret-credential.md +21 -0
  44. package/knowledge/decisions/a-wire-with-auth-disabled-cannot-point-at-a-pikkufunc.md +21 -0
  45. package/knowledge/decisions/addon-contracts-cannot-wire-or-declare-handlers.md +25 -0
  46. package/knowledge/decisions/addon-declarations-name-the-secrets-and-credentials-the-app-lends-it.md +19 -0
  47. package/knowledge/decisions/an-inline-wiring-function-is-sessionless-unless-built-with-pikkufunc.md +23 -0
  48. package/knowledge/decisions/better-auth-admin-plugin-is-refused.md +24 -0
  49. package/knowledge/decisions/define-screens-scopes-must-be-string-literals.md +18 -0
  50. package/knowledge/decisions/index.md +38 -0
  51. package/knowledge/decisions/inspector-records-a-non-literal-global-grant-as-source-text.md +20 -0
  52. package/knowledge/decisions/only-one-pikku-better-auth-export-per-codebase.md +19 -0
  53. package/knowledge/decisions/scope-references-and-secret-aliases-are-critical-diagnostics.md +23 -0
  54. package/knowledge/decisions/scope-segments-may-not-be-wildcards-or-contain-colons.md +23 -0
  55. package/knowledge/decisions/shorthand-permission-entries-are-extracted.md +22 -0
  56. package/knowledge/decisions/system-roles-declare-literal-unqualified-scopes.md +28 -0
  57. package/knowledge/decisions/the-inspector-records-agent-permissions-only-to-aggregate-services.md +14 -0
  58. package/knowledge/decisions/vault-secrets-reach-no-sink-and-pii-stays-out-of-logs-and-webhooks.md +30 -0
  59. package/knowledge/decisions/webhook-gateway-wrappers-are-sessionless-and-gateway-auth-runs-inside-the-handler.md +26 -0
  60. package/knowledge/index.md +14 -0
  61. package/knowledge/questions/does-a-non-literal-remote-addon-auth-get-checked.md +26 -0
  62. package/knowledge/questions/index.md +16 -0
  63. package/knowledge/questions/is-an-incomplete-oauth2-block-dropped-without-a-log.md +23 -0
  64. package/knowledge/questions/should-app-functions-named-like-scenario-instrumentation-be-refused.md +28 -0
  65. package/knowledge/questions/should-the-openapi-document-default-every-operation-to-a-bearer-or-api-key.md +22 -0
  66. package/package.json +3 -3
  67. 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
@@ -325,15 +325,23 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
325
325
  }
326
326
  break;
327
327
  case 'subcommands': {
328
- let subcommandsNode = prop.initializer;
329
- if (ts.isIdentifier(prop.initializer)) {
330
- subcommandsNode = resolveIdentifier(prop.initializer, typeChecker, [
331
- 'defineCLICommands',
332
- ]);
333
- }
334
- if (subcommandsNode && ts.isObjectLiteralExpression(subcommandsNode)) {
335
- meta.subcommands = {};
336
- for (const subProp of subcommandsNode.properties) {
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
- meta.subcommands[subName] = subCommand;
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', 'persona'],
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 scenarioStepRequiresActor;
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 an actor whether or not the step said so.
564
- scenarioStepRequiresActor =
565
- getPropertyValue(firstArg, 'actor') === true ||
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
- scenarioStepRequiresActor,
1087
- // `persona` is the default reading of an unmarked step, so it is left off
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 !== 'persona'
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 { isRunnablePersona } from '@pikku/core/persona';
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 persona makes, and you would read the run as a product
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, personaId, logger) => {
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, `Persona '${personaId}' has a linked account whose key is not a literal.`);
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, `Persona '${personaId}' linked account '${prop.name.text}' must be an object literal.`);
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 `definePersonas()` calls.
99
+ * Inspector for `defineVirtualUsers()` calls.
100
100
  *
101
- * A persona is entirely declaration — a person, their roles, what they want —
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 `personas.gen.json` for
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 persona without one is
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 addPersonas = (logger, node, _checker, state) => {
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 !== 'definePersonas') {
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, 'definePersonas() needs an inline object — personas are read statically, never evaluated.');
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.personas.files, ErrorCode.DUPLICATE_PERSONAS_DEFINITION, 'definePersonas', sourceFile)) {
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 persona is declared with a key that is not a literal.');
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, `Persona '${id}' must be an object literal.`);
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, `Persona '${id}' needs a literal 'name' — a persona is a person, and the console has nothing to show without one.`);
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 `definePersonas` types this as a union, so a bad value only
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, `Persona '${id}' has an unknown disposition '${disposition}'. One of: ${known.join(', ')}`);
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, `Persona '${id}': ${message}`);
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 persona = {
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: isRunnablePersona({
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
- persona.jobTitle = jobTitle;
185
+ virtualUser.jobTitle = jobTitle;
186
186
  const description = stringProperty(config, 'description');
187
187
  if (description !== undefined)
188
- persona.description = description;
188
+ virtualUser.description = description;
189
189
  const avatarUrl = stringProperty(config, 'avatarUrl');
190
190
  if (avatarUrl !== undefined)
191
- persona.avatarUrl = avatarUrl;
191
+ virtualUser.avatarUrl = avatarUrl;
192
192
  const personality = stringProperty(config, 'personality');
193
193
  if (personality !== undefined)
194
- persona.personality = personality;
194
+ virtualUser.personality = personality;
195
195
  const app = stringProperty(config, 'app');
196
196
  if (app !== undefined)
197
- persona.app = app;
197
+ virtualUser.app = app;
198
198
  if (disposition !== undefined) {
199
- persona.disposition = disposition;
199
+ virtualUser.disposition = disposition;
200
200
  }
201
201
  if (tuning !== undefined)
202
- persona.tuning = tuning;
202
+ virtualUser.tuning = tuning;
203
203
  const fixtures = stringArrayProperty(config, 'fixtures');
204
204
  if (fixtures !== undefined)
205
- persona.fixtures = fixtures;
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
- persona.environments = environments;
211
+ virtualUser.environments = environments;
212
212
  if (account !== undefined)
213
- persona.account = account;
213
+ virtualUser.account = account;
214
214
  const linkedAccounts = linkedAccountsProperty(config, id, logger);
215
215
  if (linkedAccounts !== undefined)
216
- persona.linkedAccounts = linkedAccounts;
217
- state.personas.definitions.push(persona);
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) || expression.text !== 'wireAddon')
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 wireAddon: ${name} → ${pkg}`);
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);