@pikku/cli 0.12.85 → 0.12.88

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 (138) hide show
  1. package/console-app/assets/{index-CEv-kj3J.js → index-C3FPwdl7.js} +161 -147
  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-contracts-meta.gen.json +4 -0
  12. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +4 -0
  16. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  17. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  18. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  19. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  20. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  21. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  22. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  23. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  24. package/dist/.pikku/function/pikku-functions-meta.gen.json +41 -17
  25. package/dist/.pikku/function/pikku-functions.gen.js +3 -1
  26. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  27. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  28. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  30. package/dist/.pikku/pikku-bootstrap.gen.d.ts +3 -1
  31. package/dist/.pikku/pikku-bootstrap.gen.js +3 -1
  32. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  34. package/dist/.pikku/pikku-services.gen.d.ts +2 -1
  35. package/dist/.pikku/pikku-services.gen.js +1 -0
  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.d.ts +1 -0
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +6 -0
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.json +6 -0
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +4 -0
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +5 -0
  45. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -0
  47. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  48. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  49. package/dist/.pikku/schemas/register.gen.js +3 -1
  50. package/dist/.pikku/schemas/schemas/FabricLoginInput.schema.json +1 -1
  51. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  52. package/dist/.pikku/schemas/schemas/PikkuUserAdminFunctionsOutput.schema.json +1 -0
  53. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
  54. package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
  55. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  56. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  57. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  58. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  59. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  60. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  61. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  62. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  63. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  64. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  65. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  66. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +8 -2
  67. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  68. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  69. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  70. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  71. package/dist/bin/pikku-bin.mjs +2 -2
  72. package/dist/bin/pikku.js +22 -0
  73. package/dist/src/fabric/fabric-commands.d.ts +6 -3
  74. package/dist/src/fabric/fabric-commands.js +4 -0
  75. package/dist/src/fabric/functions/deploy.function.js +6 -0
  76. package/dist/src/fabric/functions/init.function.js +2 -2
  77. package/dist/src/fabric/functions/llm-key.function.d.ts +4 -4
  78. package/dist/src/fabric/functions/login.function.d.ts +4 -0
  79. package/dist/src/fabric/functions/login.function.js +45 -4
  80. package/dist/src/fabric/functions/validate.function.js +12 -2
  81. package/dist/src/fabric/lib/config.d.ts +8 -0
  82. package/dist/src/fabric/lib/config.js +15 -1
  83. package/dist/src/fabric/lib/git.js +2 -2
  84. package/dist/src/functions/wirings/ai-agent/pikku-command-public-agent.js +6 -2
  85. package/dist/src/functions/wirings/ai-agent/serialize-public-agent.d.ts +5 -1
  86. package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +81 -48
  87. package/dist/src/functions/wirings/auth/pikku-command-auth.js +4 -0
  88. package/dist/src/functions/wirings/console/pikku-command-console-functions.js +6 -2
  89. package/dist/src/functions/wirings/console/serialize-console-functions.d.ts +5 -1
  90. package/dist/src/functions/wirings/console/serialize-console-functions.js +55 -23
  91. package/dist/src/functions/wirings/functions/schemas.js +10 -1
  92. package/dist/src/functions/wirings/graph/pikku-command-graph-wirings.js +2 -0
  93. package/dist/src/functions/wirings/queue/pikku-command-queue.js +10 -2
  94. package/dist/src/functions/wirings/realtime/pikku-command-events-scaffold.js +8 -2
  95. package/dist/src/functions/wirings/realtime/serialize-events-scaffold.d.ts +5 -4
  96. package/dist/src/functions/wirings/realtime/serialize-events-scaffold.js +17 -7
  97. package/dist/src/functions/wirings/rpc/pikku-command-public-rpc.js +6 -2
  98. package/dist/src/functions/wirings/rpc/pikku-command-remote-rpc.js +6 -2
  99. package/dist/src/functions/wirings/rpc/serialize-public-rpc.d.ts +5 -1
  100. package/dist/src/functions/wirings/rpc/serialize-public-rpc.js +16 -5
  101. package/dist/src/functions/wirings/rpc/serialize-remote-rpc.d.ts +5 -1
  102. package/dist/src/functions/wirings/rpc/serialize-remote-rpc.js +16 -5
  103. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +8 -2
  104. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +5 -1
  105. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +22 -16
  106. package/dist/src/functions/wirings/user-admin/pikku-command-user-admin-functions.d.ts +1 -0
  107. package/dist/src/functions/wirings/user-admin/pikku-command-user-admin-functions.js +49 -0
  108. package/dist/src/functions/wirings/user-admin/serialize-user-admin-functions.d.ts +24 -0
  109. package/dist/src/functions/wirings/user-admin/serialize-user-admin-functions.js +283 -0
  110. package/dist/src/functions/wirings/webhook/pikku-command-webhook.js +6 -2
  111. package/dist/src/functions/wirings/webhook/serialize-webhook.d.ts +5 -8
  112. package/dist/src/functions/wirings/webhook/serialize-webhook.js +19 -12
  113. package/dist/src/functions/wirings/workflow/pikku-command-workflow-routes.js +8 -2
  114. package/dist/src/functions/wirings/workflow/serialize-workflow-routes.d.ts +5 -1
  115. package/dist/src/functions/wirings/workflow/serialize-workflow-routes.js +61 -31
  116. package/dist/src/functions/workflows/all.workflow.js +8 -0
  117. package/dist/src/utils/pikku-cli-config.js +40 -10
  118. package/dist/src/utils/remove-legacy-scaffold-file.d.ts +3 -0
  119. package/dist/src/utils/remove-legacy-scaffold-file.js +43 -0
  120. package/dist/src/utils/serialize-schemas.js +57 -2
  121. package/dist/tsconfig.tsbuildinfo +1 -1
  122. package/package.json +7 -7
  123. package/skills/pikku-addon/SKILL.md +1 -0
  124. package/skills/pikku-ai-agent/SKILL.md +2 -1
  125. package/skills/pikku-ai-vercel/SKILL.md +1 -0
  126. package/skills/pikku-better-auth/SKILL.md +57 -0
  127. package/skills/pikku-concepts/SKILL.md +17 -1
  128. package/skills/pikku-concepts/references/concept-mapping.md +11 -9
  129. package/skills/pikku-emails/SKILL.md +1 -0
  130. package/skills/pikku-fabric/SKILL.md +92 -19
  131. package/skills/pikku-fabric-debug/SKILL.md +112 -0
  132. package/skills/pikku-permissions/SKILL.md +28 -12
  133. package/skills/pikku-react/SKILL.md +32 -3
  134. package/skills/pikku-react-query/SKILL.md +38 -1
  135. package/skills/pikku-realtime/SKILL.md +1 -1
  136. package/skills/pikku-security/SKILL.md +3 -3
  137. package/skills/pikku-template-clone/SKILL.md +1 -0
  138. package/cli.schema.json +0 -1
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { spawn } from 'node:child_process';
2
3
  import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
3
4
  import { readAuthFile, writeAuthFile, resolveApiContext, } from '../lib/config.js';
4
5
  import { getFabricRPC } from '../lib/http.js';
@@ -8,6 +9,7 @@ export const FabricLoginInput = z.object({
8
9
  token: z.string().optional(),
9
10
  apiUrl: z.string().optional(),
10
11
  consoleUrl: z.string().optional(),
12
+ browser: z.boolean().optional(),
11
13
  });
12
14
  export const FabricLoginOutput = z.object({
13
15
  ok: z.boolean(),
@@ -15,6 +17,33 @@ export const FabricLoginOutput = z.object({
15
17
  });
16
18
  const POLL_INTERVAL_MS = 2000;
17
19
  const POLL_TIMEOUT_MS = 5 * 60 * 1000;
20
+ /**
21
+ * Best-effort browser launch. The URL is always printed first, so a failure
22
+ * here costs the user a click, not the login — hence a warning rather than a
23
+ * throw. Skipped over SSH and in CI, where opening a browser on the wrong
24
+ * machine is worse than not opening one.
25
+ */
26
+ function openBrowser(url) {
27
+ if (process.env.SSH_TTY || process.env.CI)
28
+ return;
29
+ const cmd = process.platform === 'darwin'
30
+ ? 'open'
31
+ : process.platform === 'win32'
32
+ ? 'start'
33
+ : 'xdg-open';
34
+ try {
35
+ const child = spawn(cmd, [url], {
36
+ stdio: 'ignore',
37
+ detached: true,
38
+ shell: process.platform === 'win32',
39
+ });
40
+ child.on('error', (err) => console.warn(`[fabric] could not open a browser (${err.message}) — open the URL above.`));
41
+ child.unref();
42
+ }
43
+ catch (err) {
44
+ console.warn(`[fabric] could not open a browser (${err.message}) — open the URL above.`);
45
+ }
46
+ }
18
47
  /**
19
48
  * Device-authorization-style CLI login. Server mints a short code, user types
20
49
  * it on the console's /cli-auth page (logging in via Better Auth if not already
@@ -25,7 +54,7 @@ export const FabricLogin = pikkuSessionlessFunc({
25
54
  description: 'Authenticate the CLI against fabric-api via a one-time code.',
26
55
  input: FabricLoginInput,
27
56
  output: FabricLoginOutput,
28
- func: async (_services, { apiKey, token, apiUrl: apiUrlOverride, consoleUrl: consoleUrlOverride }) => {
57
+ func: async (_services, { apiKey, token, apiUrl: apiUrlOverride, consoleUrl: consoleUrlOverride, browser, }) => {
29
58
  const ctx = await resolveApiContext({ apiUrlOverride });
30
59
  const apiUrl = ctx.apiUrl;
31
60
  // Static-token fast paths — useful for CI and "I already have a token".
@@ -40,14 +69,21 @@ export const FabricLogin = pikkuSessionlessFunc({
40
69
  const consoleUrl = (consoleUrlOverride ?? deriveConsoleUrl(apiUrl)).replace(/\/$/, '');
41
70
  const rpc = getFabricRPC({ apiUrl, token: null });
42
71
  const { code, expiresAt } = await rpc.invoke('requestCliAuth');
72
+ // Deliberately no ?code= — the browser opens the page, but the code is
73
+ // typed. That hand-off is what proves the person authorizing the token is
74
+ // the person who ran this command; a link carrying its own code is a
75
+ // one-click grant for anyone who can put it in front of a signed-in user.
76
+ const authUrl = `${consoleUrl}/cli-auth`;
43
77
  console.log('');
44
- console.log(' Enter this code at:');
45
- console.log(` ${consoleUrl}/cli-auth`);
78
+ console.log(' Enter this code to finish signing in:');
79
+ console.log(` ${code}`);
46
80
  console.log('');
47
- console.log(` Code: ${code}`);
81
+ console.log(` at ${authUrl}`);
48
82
  console.log(` (expires ${new Date(expiresAt).toLocaleTimeString()})`);
49
83
  console.log('');
50
84
  console.log(' Waiting for confirmation…');
85
+ if (browser !== false)
86
+ openBrowser(authUrl);
51
87
  const deadline = Date.now() + POLL_TIMEOUT_MS;
52
88
  while (Date.now() < deadline) {
53
89
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
@@ -55,6 +91,11 @@ export const FabricLogin = pikkuSessionlessFunc({
55
91
  if (result.status === 'expired') {
56
92
  throw new Error('Code expired before confirmation. Run `pikku fabric login` again.');
57
93
  }
94
+ // Distinct from expired: the user pressed Cancel, so stop immediately
95
+ // rather than sitting here until the TTL runs out.
96
+ if (result.status === 'rejected') {
97
+ throw new Error('Sign-in was cancelled in the browser. Run `pikku fabric login` again to retry.');
98
+ }
58
99
  if (result.status === 'confirmed' && result.token) {
59
100
  const auth = await readAuthFile();
60
101
  auth.tokens[apiUrl] = result.token;
@@ -605,8 +605,12 @@ export async function runValidate(startDir = process.cwd()) {
605
605
  const pikkuGenDir = join(fnDir, '.pikku');
606
606
  const typesGenTs = join(pikkuGenDir, 'pikku-types.gen.ts');
607
607
  const typesGenJs = join(pikkuGenDir, 'pikku-types.gen.js');
608
+ // A warning, not an error: the wrapper is gitignored and recreated by
609
+ // the sandbox entrypoint and `fabric smoke`, so a freshly cloned or
610
+ // freshly scaffolded project never has it. As an error, "validate must
611
+ // pass clean" was unachievable from a clean checkout.
608
612
  if (existsSync(typesGenTs) && !existsSync(typesGenJs)) {
609
- e('pikku-types-js-wrapper-missing', 'packages/functions/.pikku/pikku-types.gen.js is missing — the sandbox runs pikku dev via bare Node.js, which cannot load .ts files; without the .js wrapper it crashes with "Cannot find module \'#pikku/pikku-types.gen.js\'" on every restart', typesGenTs, lines('Create packages/functions/.pikku/pikku-types.gen.js:', " import './pikku-types.gen.ts';", " export * from './pikku-types.gen.ts';", 'Or run `pikku fabric smoke` to test the full boot locally (it creates the wrappers).', 'This file is gitignored — it must be recreated after every `pikku all` run.'));
613
+ w('pikku-types-js-wrapper-missing', 'packages/functions/.pikku/pikku-types.gen.js is missing — the sandbox runs pikku dev via bare Node.js, which cannot load .ts files; without the .js wrapper it crashes with "Cannot find module \'#pikku/pikku-types.gen.js\'" on every restart', typesGenTs, lines('Create packages/functions/.pikku/pikku-types.gen.js:', " import './pikku-types.gen.ts';", " export * from './pikku-types.gen.ts';", 'Or run `pikku fabric smoke` to test the full boot locally (it creates the wrappers).', 'This file is gitignored — it must be recreated after every `pikku all` run.'));
610
614
  }
611
615
  }
612
616
  const fnAllDeps = {
@@ -655,7 +659,13 @@ export async function runValidate(startDir = process.cwd()) {
655
659
  const usesLibsql = servicesText.includes('@pikku/kysely-sqlite') ||
656
660
  servicesText.includes('LibsqlWebDialect');
657
661
  const usesProcessEnv = /\bprocess\.env\.[A-Z_]/.test(servicesText);
658
- if (dbEngine !== 'postgres' && usesKysely && !usesLibsql) {
662
+ // Only a services.ts that BUILDS its own client can pick the wrong
663
+ // dialect. The starter template takes kysely from injection (pikku dev
664
+ // supplies node:sqlite, the Worker supplies libsql) and constructs
665
+ // nothing — flagging it told every fresh scaffold to import a dialect it
666
+ // must not use.
667
+ const constructsKysely = /new\s+Kysely\s*[<(]/.test(servicesText);
668
+ if (dbEngine !== 'postgres' && usesKysely && constructsKysely && !usesLibsql) {
659
669
  e('services-wrong-db-adapter', 'services.ts uses Kysely but not LibsqlWebDialect — Fabric injects a Turso/libSQL DATABASE_URL at runtime, not a PostgreSQL URL', servicesPath, 'Import LibsqlWebDialect from @pikku/kysely-sqlite and replace the dialect: new Kysely({ dialect: new LibsqlWebDialect({ url: databaseUrl }) })');
660
670
  }
661
671
  if (usesProcessEnv) {
@@ -42,6 +42,14 @@ export declare function findProjectConfig(startDir?: string): Promise<{
42
42
  path: string;
43
43
  config: ProjectConfig;
44
44
  } | null>;
45
+ /**
46
+ * Templates ship `pikkufabric.config.json` with a `__PROJECT_ID__` placeholder
47
+ * so the file's shape is visible before the repo is linked. A placeholder is
48
+ * not a link: without this, `fabric init` on a fresh scaffold reports
49
+ * "Already linked: __PROJECT_ID__" and every other command sends the
50
+ * placeholder to the API as if it were a real id.
51
+ */
52
+ export declare function isLinkedProjectId(projectId?: string | null): boolean;
45
53
  export declare function writeProjectConfig(cwd: string, config: ProjectConfig): Promise<string>;
46
54
  export declare function readAuthFile(): Promise<AuthFile>;
47
55
  export declare function writeAuthFile(file: AuthFile): Promise<void>;
@@ -19,6 +19,18 @@ export async function findProjectConfig(startDir = process.cwd()) {
19
19
  dir = parent;
20
20
  }
21
21
  }
22
+ /**
23
+ * Templates ship `pikkufabric.config.json` with a `__PROJECT_ID__` placeholder
24
+ * so the file's shape is visible before the repo is linked. A placeholder is
25
+ * not a link: without this, `fabric init` on a fresh scaffold reports
26
+ * "Already linked: __PROJECT_ID__" and every other command sends the
27
+ * placeholder to the API as if it were a real id.
28
+ */
29
+ export function isLinkedProjectId(projectId) {
30
+ if (!projectId)
31
+ return false;
32
+ return !/^__.*__$/.test(projectId);
33
+ }
22
34
  export async function writeProjectConfig(cwd, config) {
23
35
  const path = join(cwd, projectConfigName);
24
36
  await mkdir(dirname(path), { recursive: true });
@@ -57,6 +69,8 @@ export async function resolveApiContext(opts = {}) {
57
69
  return {
58
70
  apiUrl,
59
71
  token: auth.tokens[apiUrl] ?? null,
60
- projectId: projectFile?.config.projectId ?? null,
72
+ projectId: isLinkedProjectId(projectFile?.config.projectId)
73
+ ? projectFile.config.projectId
74
+ : null,
61
75
  };
62
76
  }
@@ -136,8 +136,8 @@ export async function assertNamedBranchDeploySafety(branch, cwd) {
136
136
  try {
137
137
  head = await localBranchHeadSha(branch, cwd);
138
138
  }
139
- catch {
140
- throw new Error(`Deployment blocked: local branch ${branch} does not exist.\nFetch or create it before deploying.`);
139
+ catch (error) {
140
+ throw new Error(`Deployment blocked: local branch ${branch} does not exist (${error.message}).\nFetch or create it before deploying.`);
141
141
  }
142
142
  const upstream = await upstreamForBranch(branch, cwd);
143
143
  if (!upstream) {
@@ -2,12 +2,16 @@ 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 { removeLegacyScaffoldFile } from '../../../utils/remove-legacy-scaffold-file.js';
5
6
  import { serializePublicAgent } from './serialize-public-agent.js';
6
7
  export const pikkuPublicAgent = pikkuSessionlessFunc({
7
8
  func: async ({ logger, config }) => {
8
- if (config.scaffold?.agent) {
9
+ if (config.scaffold?.agent && config.publicAgentSchemasFile) {
9
10
  const pathToPikkuTypes = getFileImportRelativePath(config.publicAgentFile, config.typesDeclarationFile, config.packageMappings);
10
- await writeFileInDir(logger, config.publicAgentFile, serializePublicAgent(pathToPikkuTypes, config.scaffold.agent === 'auth', config.globalHTTPPrefix || ''));
11
+ const { schemas, functions } = serializePublicAgent(pathToPikkuTypes, config.scaffold.agent === 'auth', config.globalHTTPPrefix || '');
12
+ await writeFileInDir(logger, config.publicAgentSchemasFile, schemas);
13
+ await writeFileInDir(logger, config.publicAgentFile, functions);
14
+ await removeLegacyScaffoldFile(config.publicAgentFile);
11
15
  return true;
12
16
  }
13
17
  return false;
@@ -1 +1,5 @@
1
- export declare const serializePublicAgent: (pathToPikkuTypes: string, requireAuth?: boolean, globalHTTPPrefix?: string) => string;
1
+ export interface PublicAgentGenOutput {
2
+ schemas: string;
3
+ functions: string;
4
+ }
5
+ export declare const serializePublicAgent: (pathToPikkuTypes: string, requireAuth?: boolean, globalHTTPPrefix?: string) => PublicAgentGenOutput;
@@ -1,28 +1,69 @@
1
1
  export const serializePublicAgent = (pathToPikkuTypes, requireAuth = true, globalHTTPPrefix = '') => {
2
2
  const authFlag = requireAuth ? 'true' : 'false';
3
- // Emitted inline at each call site rather than as a shared named alias: the
4
- // schema extractor only reads type literals in the generic position, and
5
- // synthesises the schema name from the function name. Behind a named alias it
6
- // records an `inputSchemaName` with no schema behind it, and every agent call
7
- // then fails at runtime with MissingSchemaError.
8
- const callerInput = `{
9
- agentName: string
10
- message: string
11
- threadId: string
12
- resourceId: string
13
- attachments?: {
14
- type: 'image' | 'file'
15
- data?: string
16
- url?: string
17
- mediaType?: string
18
- filename?: string
19
- }[]
20
- model?: string
21
- temperature?: number
22
- context?: string
23
- }`;
24
- return `import { canAccessThread, threadOwnerConstraint } from '@pikku/core/ai-agent'
3
+ const schemas = `/**
4
+ * Auto-generated public agent schemas
5
+ * Do not edit manually - regenerate with 'npx pikku'
6
+ */
7
+ import { z } from 'zod'
8
+
9
+ export const Attachment = z.object({
10
+ type: z.enum(['image', 'file']),
11
+ data: z.string().optional(),
12
+ url: z.string().optional(),
13
+ mediaType: z.string().optional(),
14
+ filename: z.string().optional(),
15
+ })
16
+
17
+ export const AgentCall = z.object({
18
+ agentName: z.string(),
19
+ message: z.string(),
20
+ threadId: z.string(),
21
+ resourceId: z.string(),
22
+ attachments: z.array(Attachment).optional(),
23
+ model: z.string().optional(),
24
+ temperature: z.number().optional(),
25
+ context: z.string().optional(),
26
+ })
27
+
28
+ export const AgentApproval = z.object({
29
+ agentName: z.string(),
30
+ runId: z.string(),
31
+ approvals: z.array(
32
+ z.object({ toolCallId: z.string(), approved: z.boolean() })
33
+ ),
34
+ })
35
+
36
+ export const AgentResume = z.object({
37
+ agentName: z.string(),
38
+ runId: z.string(),
39
+ toolCallId: z.string(),
40
+ approved: z.boolean(),
41
+ })
42
+
43
+ export const ThreadsQuery = z.object({
44
+ agentName: z.string().optional(),
45
+ resourceId: z.string().optional(),
46
+ limit: z.number().optional(),
47
+ offset: z.number().optional(),
48
+ })
49
+
50
+ export const ThreadRef = z.object({
51
+ threadId: z.string(),
52
+ resourceId: z.string().optional(),
53
+ })
54
+
55
+ export const ThreadDeleted = z.object({ deleted: z.boolean() })
56
+ `;
57
+ const functions = `import { canAccessThread, threadOwnerConstraint } from '@pikku/core/ai-agent'
25
58
  import { pikkuSessionlessFunc, pikkuPermission, defineHTTPRoutes, wireHTTPRoutes } from '${pathToPikkuTypes}'
59
+ import {
60
+ AgentCall,
61
+ AgentApproval,
62
+ AgentResume,
63
+ ThreadsQuery,
64
+ ThreadRef,
65
+ ThreadDeleted,
66
+ } from './agent.schemas.gen.js'
26
67
 
27
68
  /**
28
69
  * Thread reads are keyed by a caller-supplied \`threadId\`, so ownership cannot
@@ -39,9 +80,10 @@ export const isThreadOwner = pikkuPermission<{ threadId: string }>(
39
80
  }
40
81
  )
41
82
 
42
- export const agentCaller = pikkuSessionlessFunc<${callerInput}, unknown>({
83
+ export const agentCaller = pikkuSessionlessFunc({
43
84
  tags: ['pikku'],
44
85
  auth: ${authFlag},
86
+ input: AgentCall,
45
87
  func: async (_services, data, { rpc }) => {
46
88
  return await rpc.agent.run(data.agentName as any, {
47
89
  message: data.message,
@@ -55,9 +97,10 @@ export const agentCaller = pikkuSessionlessFunc<${callerInput}, unknown>({
55
97
  },
56
98
  })
57
99
 
58
- export const agentStreamCaller = pikkuSessionlessFunc<${callerInput}, void>({
100
+ export const agentStreamCaller = pikkuSessionlessFunc({
59
101
  tags: ['pikku'],
60
102
  auth: ${authFlag},
103
+ input: AgentCall,
61
104
  func: async (_services, data, { rpc }) => {
62
105
  await rpc.agent.stream(data.agentName as any, {
63
106
  message: data.message,
@@ -71,23 +114,19 @@ export const agentStreamCaller = pikkuSessionlessFunc<${callerInput}, void>({
71
114
  },
72
115
  })
73
116
 
74
- export const agentApproveCaller = pikkuSessionlessFunc<
75
- { agentName: string; runId: string; approvals: { toolCallId: string; approved: boolean }[] },
76
- unknown
77
- >({
117
+ export const agentApproveCaller = pikkuSessionlessFunc({
78
118
  tags: ['pikku'],
79
119
  auth: ${authFlag},
120
+ input: AgentApproval,
80
121
  func: async (_services, { runId, approvals, agentName }, { rpc }) => {
81
122
  return await rpc.agent.approve(runId, approvals, agentName)
82
123
  },
83
124
  })
84
125
 
85
- export const agentResumeCaller = pikkuSessionlessFunc<
86
- { agentName: string; runId: string; toolCallId: string; approved: boolean },
87
- void
88
- >({
126
+ export const agentResumeCaller = pikkuSessionlessFunc({
89
127
  tags: ['pikku'],
90
128
  auth: ${authFlag},
129
+ input: AgentResume,
91
130
  func: async (_services, data, { rpc }) => {
92
131
  await rpc.agent.resume(data.runId, {
93
132
  toolCallId: data.toolCallId,
@@ -96,11 +135,9 @@ export const agentResumeCaller = pikkuSessionlessFunc<
96
135
  },
97
136
  })
98
137
 
99
- export const getAgentThreads = pikkuSessionlessFunc<
100
- { agentName?: string; resourceId?: string; limit?: number; offset?: number },
101
- any[]
102
- >({
138
+ export const getAgentThreads = pikkuSessionlessFunc({
103
139
  tags: ['pikku', 'pikku:agent'],
140
+ input: ThreadsQuery,
104
141
  title: 'Get Agent Threads',
105
142
  description:
106
143
  'Returns the caller\\'s AI agent threads from storage. Accepts optional filters: agentName, resourceId, limit, and offset for pagination.',
@@ -119,11 +156,9 @@ export const getAgentThreads = pikkuSessionlessFunc<
119
156
  },
120
157
  })
121
158
 
122
- export const getAgentThreadMessages = pikkuSessionlessFunc<
123
- { threadId: string; resourceId?: string },
124
- any[]
125
- >({
159
+ export const getAgentThreadMessages = pikkuSessionlessFunc({
126
160
  tags: ['pikku', 'pikku:agent'],
161
+ input: ThreadRef,
127
162
  title: 'Get Agent Thread Messages',
128
163
  description:
129
164
  'Returns all messages for a given AI agent thread, ordered by creation time.',
@@ -135,11 +170,9 @@ export const getAgentThreadMessages = pikkuSessionlessFunc<
135
170
  },
136
171
  })
137
172
 
138
- export const getAgentThreadRuns = pikkuSessionlessFunc<
139
- { threadId: string; resourceId?: string },
140
- any[]
141
- >({
173
+ export const getAgentThreadRuns = pikkuSessionlessFunc({
142
174
  tags: ['pikku', 'pikku:agent'],
175
+ input: ThreadRef,
143
176
  title: 'Get Agent Thread Runs',
144
177
  description:
145
178
  'Returns the run history for a given AI agent thread, ordered by creation time.',
@@ -151,11 +184,10 @@ export const getAgentThreadRuns = pikkuSessionlessFunc<
151
184
  },
152
185
  })
153
186
 
154
- export const deleteAgentThread = pikkuSessionlessFunc<
155
- { threadId: string; resourceId?: string },
156
- { deleted: boolean }
157
- >({
187
+ export const deleteAgentThread = pikkuSessionlessFunc({
158
188
  tags: ['pikku', 'pikku:agent'],
189
+ input: ThreadRef,
190
+ output: ThreadDeleted,
159
191
  title: 'Delete Agent Thread',
160
192
  description:
161
193
  'Deletes an AI agent thread and all of its persisted state.',
@@ -199,4 +231,5 @@ export const agentRoutes = defineHTTPRoutes({
199
231
 
200
232
  wireHTTPRoutes({ routes: { agent: agentRoutes } })
201
233
  `;
234
+ return { schemas, functions };
202
235
  };
@@ -4,6 +4,7 @@ import { rm } from 'node:fs/promises';
4
4
  import { pikkuSessionlessFunc } from '#pikku';
5
5
  import { writeFileInDir } from '../../../utils/file-writer.js';
6
6
  import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
7
+ import { removeLegacyScaffoldFile } from '../../../utils/remove-legacy-scaffold-file.js';
7
8
  import { serializeAuthGen } from './serialize-auth-gen.js';
8
9
  import { serializeAuthTypes, serializeAuthTypesBootstrap, } from './serialize-auth-types.js';
9
10
  import { serializeAuthMeta } from './serialize-auth-meta.js';
@@ -49,6 +50,9 @@ export const pikkuAuth = pikkuSessionlessFunc({
49
50
  else if (existsSync(middlewareFile)) {
50
51
  await rm(middlewareFile, { force: true });
51
52
  }
53
+ for (const file of [authFile, secretsFile, middlewareFile]) {
54
+ await removeLegacyScaffoldFile(file);
55
+ }
52
56
  // Static metadata of the enabled providers/plugins for the console SSO page,
53
57
  // following the `*-meta.gen.json` convention. Read at runtime by the console
54
58
  // getAuthProviders function instead of a runtime registry.
@@ -3,6 +3,7 @@ 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
5
  import { projectDeclaresBetterAuth } from '../../../utils/detect-better-auth.js';
6
+ import { removeLegacyScaffoldFile } from '../../../utils/remove-legacy-scaffold-file.js';
6
7
  import { serializeConsoleFunctions } from './serialize-console-functions.js';
7
8
  export const pikkuConsoleFunctions = pikkuSessionlessFunc({
8
9
  func: async ({ logger, config, variables }) => {
@@ -10,7 +11,7 @@ export const pikkuConsoleFunctions = pikkuSessionlessFunc({
10
11
  if (deployCodegenFlag === '1') {
11
12
  return false;
12
13
  }
13
- if (config.scaffold?.console) {
14
+ if (config.scaffold?.console && config.consoleSchemasFile) {
14
15
  // Every console RPC now requires an authenticated session (the console is
15
16
  // an admin surface), so scaffolding it without an auth strategy produces a
16
17
  // console that 403s on every call. Better Auth is the supported strategy —
@@ -25,7 +26,10 @@ export const pikkuConsoleFunctions = pikkuSessionlessFunc({
25
26
  }
26
27
  const pathToPikkuTypes = getFileImportRelativePath(config.consoleFunctionsFile, config.typesDeclarationFile, config.packageMappings);
27
28
  const pathToAgentTypes = getFileImportRelativePath(config.consoleFunctionsFile, config.agentTypesFile, config.packageMappings);
28
- await writeFileInDir(logger, config.consoleFunctionsFile, serializeConsoleFunctions(pathToPikkuTypes, pathToAgentTypes, config.globalHTTPPrefix || ''));
29
+ const { schemas, functions } = serializeConsoleFunctions(pathToPikkuTypes, pathToAgentTypes, config.globalHTTPPrefix || '');
30
+ await writeFileInDir(logger, config.consoleSchemasFile, schemas);
31
+ await writeFileInDir(logger, config.consoleFunctionsFile, functions);
32
+ await removeLegacyScaffoldFile(config.consoleFunctionsFile);
29
33
  return true;
30
34
  }
31
35
  return false;
@@ -1 +1,5 @@
1
- export declare const serializeConsoleFunctions: (pathToPikkuTypes: string, _pathToAgentTypes: string, globalHTTPPrefix?: string) => string;
1
+ export interface ConsoleGenOutput {
2
+ schemas: string;
3
+ functions: string;
4
+ }
5
+ export declare const serializeConsoleFunctions: (pathToPikkuTypes: string, _pathToAgentTypes: string, globalHTTPPrefix?: string) => ConsoleGenOutput;
@@ -1,28 +1,62 @@
1
1
  export const serializeConsoleFunctions = (pathToPikkuTypes, _pathToAgentTypes, globalHTTPPrefix = '') => {
2
- return `import { pikkuFunc, defineHTTPRoutes, wireHTTPRoutes, ref, wireAddon } from '${pathToPikkuTypes}'
2
+ const schemas = `/**
3
+ * Auto-generated console function schemas
4
+ * Do not edit manually - regenerate with 'npx pikku'
5
+ */
6
+ import { z } from 'zod'
3
7
 
4
- export const pikkuConsoleSetSecret = pikkuFunc<{
5
- secretId: string
6
- value: unknown
7
- }, {
8
- success: boolean
9
- }>({
8
+ export const SecretRef = z.object({ secretId: z.string() })
9
+
10
+ export const SetSecret = z.object({
11
+ secretId: z.string(),
12
+ value: z.unknown(),
13
+ })
14
+
15
+ export const VariableRef = z.object({ variableId: z.string() })
16
+
17
+ export const SetVariable = z.object({
18
+ variableId: z.string(),
19
+ value: z.unknown(),
20
+ })
21
+
22
+ export const ValueResult = z.object({
23
+ exists: z.boolean(),
24
+ value: z.unknown().nullable(),
25
+ })
26
+
27
+ export const Exists = z.object({ exists: z.boolean() })
28
+
29
+ export const Success = z.object({ success: z.boolean() })
30
+ `;
31
+ const functions = `import { pikkuFunc, defineHTTPRoutes, wireHTTPRoutes, ref, wireAddon } from '${pathToPikkuTypes}'
32
+ import {
33
+ SecretRef,
34
+ SetSecret,
35
+ VariableRef,
36
+ SetVariable,
37
+ ValueResult,
38
+ Exists,
39
+ Success,
40
+ } from './console.schemas.gen.js'
41
+
42
+ export const pikkuConsoleSetSecret = pikkuFunc({
10
43
  tags: ['pikku'],
11
44
  description: 'Set the value of a secret',
12
45
  expose: true,
46
+ input: SetSecret,
47
+ output: Success,
13
48
  func: async ({ secrets }, { secretId, value }) => {
14
49
  await secrets.setSecret(secretId, value)
15
50
  return { success: true }
16
51
  },
17
52
  })
18
53
 
19
- export const pikkuConsoleGetVariable = pikkuFunc<
20
- { variableId: string },
21
- { exists: boolean; value: unknown | null }
22
- >({
54
+ export const pikkuConsoleGetVariable = pikkuFunc({
23
55
  tags: ['pikku'],
24
56
  description: 'Get the current value of a variable',
25
57
  expose: true,
58
+ input: VariableRef,
59
+ output: ValueResult,
26
60
  func: async ({ variables }, { variableId }) => {
27
61
  const exists = await variables.has(variableId)
28
62
  if (!exists) {
@@ -38,13 +72,12 @@ export const pikkuConsoleGetVariable = pikkuFunc<
38
72
  },
39
73
  })
40
74
 
41
- export const pikkuConsoleSetVariable = pikkuFunc<
42
- { variableId: string; value: unknown },
43
- { success: boolean }
44
- >({
75
+ export const pikkuConsoleSetVariable = pikkuFunc({
45
76
  tags: ['pikku'],
46
77
  description: 'Set the value of a variable',
47
78
  expose: true,
79
+ input: SetVariable,
80
+ output: Success,
48
81
  func: async ({ variables }, { variableId, value }) => {
49
82
  if (typeof value === 'string') {
50
83
  await variables.set(variableId, value)
@@ -55,26 +88,24 @@ export const pikkuConsoleSetVariable = pikkuFunc<
55
88
  },
56
89
  })
57
90
 
58
- export const pikkuConsoleHasSecret = pikkuFunc<
59
- { secretId: string },
60
- { exists: boolean }
61
- >({
91
+ export const pikkuConsoleHasSecret = pikkuFunc({
62
92
  tags: ['pikku'],
63
93
  description: 'Check if a secret exists without reading its value',
64
94
  expose: true,
95
+ input: SecretRef,
96
+ output: Exists,
65
97
  func: async ({ secrets }, { secretId }) => {
66
98
  const exists = await secrets.hasSecret(secretId)
67
99
  return { exists }
68
100
  },
69
101
  })
70
102
 
71
- export const pikkuConsoleGetSecret = pikkuFunc<
72
- { secretId: string },
73
- { exists: boolean; value: unknown | null }
74
- >({
103
+ export const pikkuConsoleGetSecret = pikkuFunc({
75
104
  tags: ['pikku'],
76
105
  description: 'Get the current value of a secret',
77
106
  expose: true,
107
+ input: SecretRef,
108
+ output: ValueResult,
78
109
  func: async ({ secrets }, { secretId }) => {
79
110
  const exists = await secrets.hasSecret(secretId)
80
111
  if (!exists) {
@@ -107,4 +138,5 @@ export const consoleRoutes = defineHTTPRoutes({
107
138
  wireAddon({ name: 'console', package: '@pikku/addon-console' })
108
139
  wireHTTPRoutes({ basePath: '${globalHTTPPrefix}', routes: { console: consoleRoutes } })
109
140
  `;
141
+ return { schemas, functions };
110
142
  };
@@ -4,7 +4,16 @@ import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-
4
4
  export const pikkuSchemas = pikkuSessionlessFunc({
5
5
  func: async ({ logger, config, getInspectorState }) => {
6
6
  const visitState = await getInspectorState();
7
- if (Object.keys(visitState.schemas).length === 0) {
7
+ // Bodies missing while functions still DECLARE schemas means the inspection pass
8
+ // was partial. Writing from it would empty register.gen.ts and delete the files
9
+ // behind schemas that are still required — unregistering the whole app off a bad
10
+ // read. Leave the previous output alone.
11
+ //
12
+ // Zero REQUIRED schemas is a different thing entirely and must still be written:
13
+ // that is a project whose last schema was removed, and it is the case where the
14
+ // files left by earlier runs have to be cleared.
15
+ if (visitState.requiredSchemas.size > 0 &&
16
+ Object.keys(visitState.schemas).length === 0) {
8
17
  return undefined;
9
18
  }
10
19
  await saveSchemas(logger, config.schemaDirectory, visitState.schemas, visitState.requiredSchemas, config.schema?.supportsImportAttributes ?? true, config.addonName || null);
@@ -2,12 +2,14 @@ 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 { removeLegacyScaffoldFile } from '../../../utils/remove-legacy-scaffold-file.js';
5
6
  import { serializeGraphWirings } from './serialize-graph-wirings.js';
6
7
  export const pikkuGraphWirings = pikkuSessionlessFunc({
7
8
  func: async ({ logger, config }) => {
8
9
  if (config.scaffold?.graph && config.graphWiringsFile) {
9
10
  const pathToPikkuTypes = getFileImportRelativePath(config.graphWiringsFile, config.typesDeclarationFile, config.packageMappings);
10
11
  await writeFileInDir(logger, config.graphWiringsFile, serializeGraphWirings(pathToPikkuTypes));
12
+ await removeLegacyScaffoldFile(config.graphWiringsFile);
11
13
  return true;
12
14
  }
13
15
  return false;
@@ -10,8 +10,16 @@ export const pikkuCommandQueue = pikkuSessionlessFunc({
10
10
  const visitState = await getInspectorState();
11
11
  const { queueWorkersWiringFile, queueWorkersWiringMetaFile, queueWorkersWiringMetaJsonFile, packageMappings, schema, } = config;
12
12
  const { queueWorkers } = visitState;
13
- if (queueWorkers.files.size === 0 ||
14
- Object.keys(queueWorkers.meta).length === 0) {
13
+ // Gate on the meta alone, never on `files`. Workflows synthesise their own
14
+ // `wf-orchestrator-*` / `wf-step-*` queue meta during post-processing, and
15
+ // those entries have no declaring source file — so a project that uses
16
+ // workflows but hand-declares no `wireQueueWorker` has a full `meta` and an
17
+ // empty `files`. Bailing on `files.size === 0` there skipped writing the
18
+ // queue meta entirely, which left `pikkuState(queue,meta)` empty at runtime:
19
+ // `getOrchestratorQueueName()` then never found a per-workflow queue and
20
+ // EVERY workflow fell back to the single shared `pikku-workflow-orchestrator`,
21
+ // where one slow job head-of-line-blocks everything queued behind it.
22
+ if (Object.keys(queueWorkers.meta).length === 0) {
15
23
  return undefined;
16
24
  }
17
25
  const fullMeta = serializeQueueMeta(queueWorkers.meta);