@pikku/cli 0.12.65 → 0.12.67

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 (76) hide show
  1. package/console-app/assets/{index-C851uJHD.js → index-CttM2Aiz.js} +141 -141
  2. package/console-app/index.html +1 -1
  3. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  4. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  6. package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  8. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  10. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  17. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  18. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  19. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  20. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  21. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  22. package/dist/.pikku/function/pikku-functions-meta.gen.json +6 -6
  23. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  24. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  25. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  26. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  27. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  28. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  31. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  33. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  35. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  37. package/dist/.pikku/pikku-types.gen.js +1 -1
  38. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  39. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  43. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  44. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  46. package/dist/.pikku/schemas/register.gen.js +1 -1
  47. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  48. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  49. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  50. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  51. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  52. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  53. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  54. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  55. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  56. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  57. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  58. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  59. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  60. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  61. package/dist/bin/pikku-bin.mjs +2 -2
  62. package/dist/src/fabric/functions/validate.function.js +1 -1
  63. package/dist/src/functions/commands/audit.js +25 -8
  64. package/dist/src/functions/commands/console.js +1 -1
  65. package/dist/src/functions/commands/enable.js +4 -4
  66. package/dist/src/functions/validate/workspace-validate.js +1 -1
  67. package/dist/src/functions/wirings/console/pikku-command-console-functions.js +13 -0
  68. package/dist/src/functions/wirings/console/pikku-command-nodes-meta.js +5 -1
  69. package/dist/src/functions/wirings/console/serialize-console-functions.js +6 -11
  70. package/dist/src/functions/wirings/functions/pikku-command-functions.js +5 -3
  71. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +5 -1
  72. package/dist/src/utils/strip-verbose-meta.d.ts +3 -0
  73. package/dist/src/utils/strip-verbose-meta.js +8 -0
  74. package/package.json +3 -3
  75. package/skills/pikku-better-auth/SKILL.md +4 -0
  76. package/skills/pikku-deps/SKILL.md +91 -0
@@ -4,7 +4,7 @@ import { pikkuVoidFunc } from '#pikku';
4
4
  const FEATURE_DEFAULTS = {
5
5
  rpc: 'auth',
6
6
  agent: 'auth',
7
- console: 'no-auth',
7
+ console: 'auth',
8
8
  workflow: 'auth',
9
9
  events: 'auth',
10
10
  remoteRpc: 'no-auth',
@@ -20,9 +20,9 @@ async function enableFeature(feature, logger, config, data) {
20
20
  if (!json.scaffold.pikkuDir) {
21
21
  json.scaffold.pikkuDir = 'pikku';
22
22
  }
23
- const value = noAuth
24
- ? 'no-auth'
25
- : FEATURE_DEFAULTS[feature];
23
+ // The console is an admin surface — every RPC requires a session, so it can
24
+ // never be scaffolded no-auth (the --no-auth flag is ignored for it).
25
+ const value = feature === 'console' ? 'auth' : noAuth ? 'no-auth' : FEATURE_DEFAULTS[feature];
26
26
  json.scaffold[feature] = value;
27
27
  await writeFile(configPath, JSON.stringify(json, null, 2) + '\n', 'utf-8');
28
28
  logger.info(`Enabled scaffold.${feature} = '${value}' in ${configPath}`);
@@ -97,7 +97,7 @@ export async function runWorkspaceValidate(startDir = process.cwd()) {
97
97
  }
98
98
  const scaffold = pikkuConfig.scaffold;
99
99
  if (!scaffold?.console) {
100
- e('pikku-config-no-console-scaffold', 'pikku.config.json missing "scaffold.console" — Fabric cannot introspect the running app (console:getFunctionsMeta and friends 404), so the sandbox builder shows no functions', pikkuConfigPath, 'Add "console": "no-auth" under "scaffold" in pikku.config.json (use "auth" to require a session)');
100
+ e('pikku-config-no-console-scaffold', 'pikku.config.json missing "scaffold.console" — Fabric cannot introspect the running app (console:getFunctionsMeta and friends 404), so the sandbox builder shows no functions', pikkuConfigPath, 'Add "console": "auth" under "scaffold" in pikku.config.json the console requires an authenticated session, so Better Auth must be set up (see the pikku-better-auth skill)');
101
101
  }
102
102
  }
103
103
  const rootPkgPath = join(root, 'package.json');
@@ -2,6 +2,7 @@ import { pikkuSessionlessFunc } from '#pikku';
2
2
  import { getFileImportRelativePath } from '../../../utils/file-import-path.js';
3
3
  import { writeFileInDir } from '../../../utils/file-writer.js';
4
4
  import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
5
+ import { projectDeclaresBetterAuth } from '../../../utils/detect-better-auth.js';
5
6
  import { serializeConsoleFunctions } from './serialize-console-functions.js';
6
7
  export const pikkuConsoleFunctions = pikkuSessionlessFunc({
7
8
  func: async ({ logger, config, variables }) => {
@@ -10,6 +11,18 @@ export const pikkuConsoleFunctions = pikkuSessionlessFunc({
10
11
  return false;
11
12
  }
12
13
  if (config.scaffold?.console) {
14
+ // Every console RPC now requires an authenticated session (the console is
15
+ // an admin surface), so scaffolding it without an auth strategy produces a
16
+ // console that 403s on every call. Better Auth is the supported strategy —
17
+ // fail fast rather than ship a dead console.
18
+ const hasBetterAuth = await projectDeclaresBetterAuth(config.rootDir, config.srcDirectories, config.ignoreFiles);
19
+ if (!hasBetterAuth) {
20
+ throw new Error(`"scaffold.console" is enabled but no pikkuBetterAuth(...) was found in the project.\n` +
21
+ `The console addon requires an authenticated session on every RPC — a console without auth ` +
22
+ `returns 403 on every call.\n` +
23
+ `Fix: set up Better Auth (add a pikkuBetterAuth(...) config) before scaffolding the console, ` +
24
+ `or remove "scaffold.console" from pikku.config.json.`);
25
+ }
13
26
  const pathToPikkuTypes = getFileImportRelativePath(config.consoleFunctionsFile, config.typesDeclarationFile, config.packageMappings);
14
27
  const pathToAgentTypes = getFileImportRelativePath(config.consoleFunctionsFile, config.agentTypesFile, config.packageMappings);
15
28
  await writeFileInDir(logger, config.consoleFunctionsFile, serializeConsoleFunctions(pathToPikkuTypes, pathToAgentTypes, config.globalHTTPPrefix || ''));
@@ -34,7 +34,8 @@ export const pikkuNodesMeta = pikkuSessionlessFunc({
34
34
  const hasNodes = Object.keys(nodes.meta).length > 0;
35
35
  const hasSecrets = secrets.definitions.length > 0;
36
36
  const hasPackageMeta = !!addonMeta?.icon || !!addonMeta?.displayName;
37
- if (!hasNodes && !hasSecrets && !hasPackageMeta) {
37
+ const hasParentServices = (state.addonRequiredParentServices?.length ?? 0) > 0;
38
+ if (!hasNodes && !hasSecrets && !hasPackageMeta && !hasParentServices) {
38
39
  return undefined;
39
40
  }
40
41
  const allowedCategories = addonMeta?.categories;
@@ -68,6 +69,9 @@ export const pikkuNodesMeta = pikkuSessionlessFunc({
68
69
  if (serverlessIncompatible && serverlessIncompatible.length > 0) {
69
70
  metaData.serverlessIncompatible = serverlessIncompatible;
70
71
  }
72
+ if (hasParentServices) {
73
+ metaData.requiredParentServices = state.addonRequiredParentServices;
74
+ }
71
75
  if (addonMetaJsonFile && (config.scaffold?.console || config.addon)) {
72
76
  const minimalMeta = stripVerboseFields(metaData);
73
77
  await writeFileInDir(logger, addonMetaJsonFile, JSON.stringify(minimalMeta, null, 2), { ignoreModifyComment: true });
@@ -1,7 +1,7 @@
1
1
  export const serializeConsoleFunctions = (pathToPikkuTypes, _pathToAgentTypes, globalHTTPPrefix = '') => {
2
- return `import { pikkuSessionlessFunc, defineHTTPRoutes, wireHTTPRoutes, ref, wireAddon } from '${pathToPikkuTypes}'
2
+ return `import { pikkuFunc, defineHTTPRoutes, wireHTTPRoutes, ref, wireAddon } from '${pathToPikkuTypes}'
3
3
 
4
- export const pikkuConsoleSetSecret = pikkuSessionlessFunc<{
4
+ export const pikkuConsoleSetSecret = pikkuFunc<{
5
5
  secretId: string
6
6
  value: unknown
7
7
  }, {
@@ -10,21 +10,19 @@ export const pikkuConsoleSetSecret = pikkuSessionlessFunc<{
10
10
  tags: ['pikku'],
11
11
  description: 'Set the value of a secret',
12
12
  expose: true,
13
- auth: false,
14
13
  func: async ({ secrets }, { secretId, value }) => {
15
14
  await secrets.setSecret(secretId, value)
16
15
  return { success: true }
17
16
  },
18
17
  })
19
18
 
20
- export const pikkuConsoleGetVariable = pikkuSessionlessFunc<
19
+ export const pikkuConsoleGetVariable = pikkuFunc<
21
20
  { variableId: string },
22
21
  { exists: boolean; value: unknown | null }
23
22
  >({
24
23
  tags: ['pikku'],
25
24
  description: 'Get the current value of a variable',
26
25
  expose: true,
27
- auth: false,
28
26
  func: async ({ variables }, { variableId }) => {
29
27
  const exists = await variables.has(variableId)
30
28
  if (!exists) {
@@ -40,14 +38,13 @@ export const pikkuConsoleGetVariable = pikkuSessionlessFunc<
40
38
  },
41
39
  })
42
40
 
43
- export const pikkuConsoleSetVariable = pikkuSessionlessFunc<
41
+ export const pikkuConsoleSetVariable = pikkuFunc<
44
42
  { variableId: string; value: unknown },
45
43
  { success: boolean }
46
44
  >({
47
45
  tags: ['pikku'],
48
46
  description: 'Set the value of a variable',
49
47
  expose: true,
50
- auth: false,
51
48
  func: async ({ variables }, { variableId, value }) => {
52
49
  if (typeof value === 'string') {
53
50
  await variables.set(variableId, value)
@@ -58,28 +55,26 @@ export const pikkuConsoleSetVariable = pikkuSessionlessFunc<
58
55
  },
59
56
  })
60
57
 
61
- export const pikkuConsoleHasSecret = pikkuSessionlessFunc<
58
+ export const pikkuConsoleHasSecret = pikkuFunc<
62
59
  { secretId: string },
63
60
  { exists: boolean }
64
61
  >({
65
62
  tags: ['pikku'],
66
63
  description: 'Check if a secret exists without reading its value',
67
64
  expose: true,
68
- auth: false,
69
65
  func: async ({ secrets }, { secretId }) => {
70
66
  const exists = await secrets.hasSecret(secretId)
71
67
  return { exists }
72
68
  },
73
69
  })
74
70
 
75
- export const pikkuConsoleGetSecret = pikkuSessionlessFunc<
71
+ export const pikkuConsoleGetSecret = pikkuFunc<
76
72
  { secretId: string },
77
73
  { exists: boolean; value: unknown | null }
78
74
  >({
79
75
  tags: ['pikku'],
80
76
  description: 'Get the current value of a secret',
81
77
  expose: true,
82
- auth: false,
83
78
  func: async ({ secrets }, { secretId }) => {
84
79
  const exists = await secrets.hasSecret(secretId)
85
80
  if (!exists) {
@@ -3,13 +3,15 @@ import { writeFileInDir } from '../../../utils/file-writer.js';
3
3
  import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
4
4
  import { serializeFunctionImports } from './serialize-function-imports.js';
5
5
  import { getFileImportRelativePath } from '../../../utils/file-import-path.js';
6
- import { stripVerboseFields, hasVerboseFields, } from '../../../utils/strip-verbose-meta.js';
6
+ import { stripVerboseFields, hasVerboseFields, reattachFunctionServices, } from '../../../utils/strip-verbose-meta.js';
7
7
  export const pikkuFunctions = pikkuSessionlessFunc({
8
8
  func: async ({ logger, config, getInspectorState }) => {
9
9
  const { functions, rpc } = await getInspectorState();
10
10
  const { functionsMetaFile, functionsMetaJsonFile, functionsFile, packageMappings, schema, } = config;
11
- // Write minimal JSON (runtime-only fields)
12
- const minimalMeta = stripVerboseFields(functions.meta);
11
+ let minimalMeta = stripVerboseFields(functions.meta);
12
+ if (config.addonName) {
13
+ minimalMeta = reattachFunctionServices(minimalMeta, functions.meta);
14
+ }
13
15
  await writeFileInDir(logger, functionsMetaJsonFile, JSON.stringify(minimalMeta, null, 2));
14
16
  // Write verbose JSON only if it has additional fields
15
17
  if (hasVerboseFields(functions.meta)) {
@@ -10,7 +10,7 @@ import { serializeWorkflowBootstrapMap } from './serialize-workflow-bootstrap-ma
10
10
  import { serializeWorkflowMeta } from './serialize-workflow-meta.js';
11
11
  import { getFileImportRelativePath } from '../../../utils/file-import-path.js';
12
12
  import { stripVerboseFields, hasVerboseFields, } from '../../../utils/strip-verbose-meta.js';
13
- import { join } from 'path';
13
+ import { join, dirname } from 'path';
14
14
  export const pikkuWorkflow = pikkuSessionlessFunc({
15
15
  func: async ({ logger, config, getInspectorState }, input) => {
16
16
  const bootstrap = input?.bootstrap === true;
@@ -66,6 +66,10 @@ export const pikkuWorkflow = pikkuSessionlessFunc({
66
66
  const userFlowActors = config.userFlows?.actors;
67
67
  if (userFlowActors && Object.keys(userFlowActors).length > 0) {
68
68
  await writeFileInDir(logger, config.userFlowActorsFile, serializeUserFlowActors(userFlowActors));
69
+ // JSON twin for the runtime meta service (console personas view). Lives
70
+ // next to workflow/meta but NOT inside it — getWorkflowMeta() treats
71
+ // every workflow/meta/*.gen.json as a workflow.
72
+ await writeFileInDir(logger, join(dirname(config.userFlowActorsFile), 'user-flow-actors.gen.json'), JSON.stringify(userFlowActors, null, 2));
69
73
  }
70
74
  await writeFileInDir(logger, workflowMapDeclarationFile, bootstrap
71
75
  ? serializeWorkflowBootstrapMap(workflows.meta, workflows.graphMeta)
@@ -29,6 +29,9 @@
29
29
  * Also strips addon namespace prefixes from pikkuFuncId values where packageName is present.
30
30
  */
31
31
  export declare function stripVerboseFields<T>(obj: T): T;
32
+ export declare function reattachFunctionServices<T extends Record<string, {
33
+ services?: unknown;
34
+ }>>(minimalMeta: T, fullMeta: T): T;
32
35
  /**
33
36
  * Check if an object has any verbose fields that would be stripped
34
37
  */
@@ -179,6 +179,14 @@ function _stripVerboseFieldsInner(obj) {
179
179
  }
180
180
  return result;
181
181
  }
182
+ export function reattachFunctionServices(minimalMeta, fullMeta) {
183
+ for (const [id, meta] of Object.entries(fullMeta)) {
184
+ if (meta?.services && minimalMeta[id]) {
185
+ minimalMeta[id].services = meta.services;
186
+ }
187
+ }
188
+ return minimalMeta;
189
+ }
182
190
  /**
183
191
  * Check if an object has any verbose fields that would be stripped
184
192
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/cli",
3
- "version": "0.12.65",
3
+ "version": "0.12.67",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "imports": {
@@ -29,10 +29,10 @@
29
29
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
30
30
  "@pikku/better-auth": "^0.12.14",
31
31
  "@pikku/bun-server": "^0.12.2",
32
- "@pikku/core": "^0.12.49",
32
+ "@pikku/core": "^0.12.51",
33
33
  "@pikku/deploy-cloudflare": "^0.12.8",
34
34
  "@pikku/fetch": "^0.12.6",
35
- "@pikku/inspector": "^0.12.33",
35
+ "@pikku/inspector": "^0.12.35",
36
36
  "@pikku/kysely": "^0.13.0",
37
37
  "@pikku/kysely-node-sqlite": "^0.12.3",
38
38
  "@pikku/node-http-server": "^0.12.4",
@@ -52,6 +52,10 @@ Better Auth owns its own HTTP surface, database tables, and session cookie. The
52
52
 
53
53
  You do NOT hand-write routes, the session middleware, or the secret wiring — `pikkuBetterAuth` + the CLI generate all of it. Re-run `pikku auth` (or `pikku all`) to regenerate.
54
54
 
55
+ ### The console requires Better Auth
56
+
57
+ The Pikku console (`@pikku/addon-console`, enabled via `scaffold.console` in `pikku.config.json`) is an admin surface: **every console RPC now requires an authenticated session** (the functions are `pikkuFunc`; unauthenticated calls return `403`). So `scaffold.console` alone is **no longer the minimum** — you also need an auth strategy, and Better Auth is the supported one. `pikku all` **throws** if `scaffold.console` is set but no `pikkuBetterAuth(...)` is found in the project. Baseline is "must be logged in"; finer policy (admin-only, org scoping) is layered host-side via tag/HTTP middleware. See `pikku-deps` for the console's Security screen.
58
+
55
59
  ---
56
60
 
57
61
  ## Standard Setup
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: pikku-deps
3
+ description: 'Use for the Pikku dependency security audit: the `pikku audit` CLI command, the `.pikku/audit.json` artifact, the `SecurityAuditReport` type in @pikku/core, and the console Security screen (getSecurityAudit / runSecurityAudit / updateDependency + SecurityAuditView).
4
+ TRIGGER when: user asks about `pikku audit`, dependency vulnerabilities/advisories, outdated dependencies, the Security screen/page in the console, updating a vulnerable dependency, or reading/rendering audit.json.
5
+ DO NOT TRIGGER when: user asks about authentication/sessions/JWT (use pikku-security), permissions (use pikku-permissions), or secrets/env vars (use pikku-config).'
6
+ installGroups: [core]
7
+ ---
8
+
9
+ # Pikku Dependency Audit
10
+
11
+ ## Agent Operating Procedure
12
+
13
+ 1. The audit is a generated artifact, not live state. `pikku audit` writes the
14
+ normalised report to `.pikku/audit.json` (config `outDir`), so it rides the
15
+ same meta pipeline as every other codegen output — uploaded on deploy,
16
+ readable by the console addon and any tooling. Read it via
17
+ `metaService.readFile('audit.json')`, never by shelling out to the package
18
+ manager from a function.
19
+ 2. One source of truth for the shape: `SecurityAuditReport` (and
20
+ `SecurityAuditIssue` / `SecurityAuditUpdate` / `SecurityAuditSummary` +
21
+ `SecuritySeverity` / `SecurityUpdateLevel`) are exported from **@pikku/core**.
22
+ The CLI writes it, the addon reads it, the UI renders it — never redeclare
23
+ the type at a call site.
24
+ 3. Validate with `pikku tsc` after changes; the command never fails a build
25
+ (advisories are informational; a missing/failed audit yields an
26
+ empty-but-valid report).
27
+
28
+ ## The `pikku audit` command
29
+
30
+ - `pikku audit` — reports **security advisories** only.
31
+ - `pikku audit --outdated` — also reports **available dependency updates**.
32
+ - Package-manager detection is by **lockfile** (walks up: `bun.lock`/`bun.lockb`,
33
+ then `yarn.lock`). Only **bun** runs a real audit (`bun audit --json` +
34
+ `bun outdated`, normalised into one `SecurityAuditReport` with per-severity /
35
+ per-update-level counts). Other PMs are detected but **stubbed** with a `note`
36
+ field until their shapes are normalised — issues/updates come back empty.
37
+ - `bun audit` exits non-zero when it *finds* advisories but still writes a valid
38
+ report — treat any non-zero exit as data, not failure.
39
+
40
+ ## Console integration (@pikku/addon-console)
41
+
42
+ Three RPCs, all reading/writing the same artifact via the meta service. Shared
43
+ spawn/read helpers live in `lib/audit-exec.ts` (`readAuditReport`,
44
+ `runPikkuAudit`, `spawnProcess`, `findBin`) — reuse them, don't re-implement.
45
+ Like every console RPC these require an **authenticated session** (the console
46
+ is admin-only), so the host must have Better Auth wired — see `pikku-better-auth`.
47
+
48
+ - `getSecurityAudit` — reads `.pikku/audit.json`, returns the report (or `null`).
49
+ - `runSecurityAudit` — runs `pikku audit --outdated` server-side (regenerates the
50
+ artifact) then returns the fresh report. Same shape as the Run Tests action.
51
+ - `updateDependency({ package, version })` — bumps the package in `package.json`
52
+ (preserving the `^`/`~` range prefix), runs `bun install`, re-audits, and
53
+ returns the fresh report. Throws if the package is not a direct dependency.
54
+ NOTE: `bun install` must be scoped to a standalone project — do not run it
55
+ inside a yarn/bun monorepo member (it resolves the whole workspace).
56
+
57
+ ## Console UI (@pikku/console)
58
+
59
+ - `SecurityPage` — the page: **Run audit** button (`lead`) + responsive
60
+ `ShellHeader` (structured `search` + `selection` for the Issues/Dependencies
61
+ lens; never cram raw controls into the non-collapsing `filters`/`view` escape
62
+ hatch). Empty state until an audit has run.
63
+ - `SecurityAuditView` — exported presentational component. Two lenses
64
+ (Issues grouped by severity; Dependencies table). Each finding row carries its
65
+ actions **right-aligned in the row header** (`Accordion.Control` sibling, so a
66
+ click acts instead of toggling): "View advisory" + a per-finding
67
+ **remediation slot**.
68
+ - `renderRemediation({ pkg, version, issue })` — the extension seam. OSS default
69
+ is `UpdateDependencyButton` (the free bump + `bun install`). Downstream
70
+ consoles (Fabric) pass their own sandbox-verified action here — replace the
71
+ action, keep the view.
72
+ - Hooks: `useSecurityAudit` (read), `useRunSecurityAudit` (run),
73
+ `useUpdateDependency` (bump). All are `useMutation`/`useQuery` — surface
74
+ `mutation.error`, never hand-roll loading/error state or swallow the error.
75
+
76
+ ## Report shape (SecurityAuditReport)
77
+
78
+ ```ts
79
+ {
80
+ tool: string // e.g. 'bun'
81
+ note?: string // set when the audit could NOT run (unsupported PM);
82
+ // render ONLY the note — never a reassuring "no vulnerabilities"
83
+ summary: { critical, high, moderate, low, info: number }
84
+ issues: SecurityAuditIssue[] // package, severity, title, advisoryId, cwe[], cvssScore?,
85
+ // url?, vulnerableVersions, recommendedVersion?
86
+ updates: SecurityAuditUpdate[] // package, current, latest, level (major|minor|patch|unknown)
87
+ }
88
+ ```
89
+
90
+ When `note` is present the audit did not run — show only the note (an "Audit not
91
+ run" state), never the "no known vulnerabilities / up to date" copy.