@pikku/cli 0.12.23 → 0.12.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.schema.json +1 -1
- package/console-app/assets/index-D4DgafuS.js +232 -0
- package/console-app/index.html +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +16 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +41 -0
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +130 -66
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +3 -1
- package/dist/.pikku/pikku-services.gen.js +2 -0
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +20 -17
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +13 -3
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuEmailsOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuFunctionTypesSplitInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuTriggerTypesInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/WorkspaceValidateInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/WorkspaceValidateOutput.schema.json +1 -0
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/meta/allWorkflow.gen.json +5 -5
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/cli.wiring.js +31 -0
- package/dist/src/fabric/functions/validate-core.d.ts +20 -0
- package/dist/src/fabric/functions/validate-core.js +227 -0
- package/dist/src/fabric/functions/validate.function.js +11 -3
- package/dist/src/functions/commands/bootstrap.js +2 -2
- package/dist/src/functions/commands/console.js +7 -4
- package/dist/src/functions/commands/db-migrate.js +2 -3
- package/dist/src/functions/commands/db-reset.js +3 -4
- package/dist/src/functions/commands/db-seed.js +2 -3
- package/dist/src/functions/commands/db-shared.d.ts +2 -15
- package/dist/src/functions/commands/db-shared.js +43 -17
- package/dist/src/functions/commands/dev.js +28 -8
- package/dist/src/functions/commands/emails-init.d.ts +5 -0
- package/dist/src/functions/commands/emails-init.js +162 -0
- package/dist/src/functions/commands/load-user-project.js +12 -3
- package/dist/src/functions/commands/watch.js +7 -4
- package/dist/src/functions/commands/workspace-validate.d.ts +33 -0
- package/dist/src/functions/commands/workspace-validate.js +9 -0
- package/dist/src/functions/db/coercion-plugin.d.ts +7 -0
- package/dist/src/functions/db/coercion-plugin.js +99 -0
- package/dist/src/functions/db/local-db.d.ts +2 -2
- package/dist/src/functions/db/local-db.js +13 -8
- package/dist/src/functions/db/seed.d.ts +2 -2
- package/dist/src/functions/db/sql-migrator.d.ts +3 -3
- package/dist/src/functions/db/sqlite-codegen.d.ts +3 -3
- package/dist/src/functions/db/sqlite-kysely.d.ts +8 -0
- package/dist/src/functions/db/sqlite-kysely.js +62 -0
- package/dist/src/functions/db/sqlite-runtime-bun.d.ts +2 -0
- package/dist/src/functions/db/sqlite-runtime-bun.js +52 -0
- package/dist/src/functions/db/sqlite-runtime-node.d.ts +2 -0
- package/dist/src/functions/db/sqlite-runtime-node.js +51 -0
- package/dist/src/functions/db/sqlite-runtime.d.ts +20 -0
- package/dist/src/functions/db/sqlite-runtime.js +13 -0
- package/dist/src/functions/validate/workspace-validate.d.ts +34 -0
- package/dist/src/functions/validate/workspace-validate.js +258 -0
- package/dist/src/functions/wirings/cli/pikku-command-cli-types.js +1 -1
- package/dist/src/functions/wirings/emails/pikku-command-emails.d.ts +6 -0
- package/dist/src/functions/wirings/emails/pikku-command-emails.js +172 -0
- package/dist/src/functions/wirings/emails/serialize-emails.d.ts +20 -0
- package/dist/src/functions/wirings/emails/serialize-emails.js +168 -0
- package/dist/src/functions/wirings/functions/pikku-command-function-types-split.d.ts +7 -1
- package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +2 -2
- package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.d.ts +7 -1
- package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.js +2 -2
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +1 -1
- package/dist/src/functions/workflows/all.workflow.js +12 -7
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/src/utils/pikku-cli-config.js +6 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/skills/pikku-auth-js/SKILL.md +271 -58
- package/console-app/assets/index-CAk106ji.js +0 -232
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
5
|
+
import { generateEmailsArtifacts } from '../wirings/emails/pikku-command-emails.js';
|
|
6
|
+
const DEFAULT_EMAIL_DIR = 'emails';
|
|
7
|
+
const DEFAULT_THEME = {
|
|
8
|
+
appName: 'Pikku App',
|
|
9
|
+
previewText: 'Your app can render localized emails out of the box.',
|
|
10
|
+
colors: {
|
|
11
|
+
background: '#f5f7fb',
|
|
12
|
+
surface: '#ffffff',
|
|
13
|
+
text: '#101828',
|
|
14
|
+
muted: '#475467',
|
|
15
|
+
border: '#d0d5dd',
|
|
16
|
+
primary: '#7c3aed',
|
|
17
|
+
primaryText: '#ffffff',
|
|
18
|
+
footer: '#667085',
|
|
19
|
+
},
|
|
20
|
+
fonts: {
|
|
21
|
+
body: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const DEFAULT_EN_LOCALE = {
|
|
25
|
+
helloWorld: {
|
|
26
|
+
subject: 'Hello from {{appName}}',
|
|
27
|
+
eyebrow: 'Email templates are live',
|
|
28
|
+
title: 'Your first Pikku email is ready',
|
|
29
|
+
intro: 'This starter template proves the pipeline works and gives you a place to shape your own visual language.',
|
|
30
|
+
body: 'Chat to your AI to create new emails, refine the theme, or localize every message for your product.',
|
|
31
|
+
cta: 'Open the email console',
|
|
32
|
+
footer: 'Built with Pikku email templates.',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const DEFAULT_DE_LOCALE = {
|
|
36
|
+
helloWorld: {
|
|
37
|
+
subject: 'Hallo von {{appName}}',
|
|
38
|
+
eyebrow: 'E-Mail-Vorlagen sind aktiv',
|
|
39
|
+
title: 'Deine erste Pikku-E-Mail ist bereit',
|
|
40
|
+
intro: 'Diese Startvorlage zeigt, dass die Pipeline funktioniert, und gibt dir einen Ausgangspunkt fuer dein eigenes Design.',
|
|
41
|
+
body: 'Sprich mit deiner KI, um neue E-Mails zu erstellen, das Theme zu verfeinern oder jede Nachricht zu lokalisieren.',
|
|
42
|
+
cta: 'E-Mail-Konsole oeffnen',
|
|
43
|
+
footer: 'Erstellt mit Pikku-E-Mail-Vorlagen.',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
function layoutTemplate() {
|
|
47
|
+
return `<!doctype html>
|
|
48
|
+
<html lang="{{locale}}">
|
|
49
|
+
<head>
|
|
50
|
+
<meta charset="utf-8" />
|
|
51
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
52
|
+
<title>{{subject}}</title>
|
|
53
|
+
</head>
|
|
54
|
+
<body style="margin:0;padding:32px 16px;background:{{theme.colors.background}};font-family:{{theme.fonts.body}};color:{{theme.colors.text}};">
|
|
55
|
+
<div style="max-width:640px;margin:0 auto;background:{{theme.colors.surface}};border:1px solid {{theme.colors.border}};border-radius:20px;overflow:hidden;">
|
|
56
|
+
{{content}}
|
|
57
|
+
</div>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
function footerPartial() {
|
|
63
|
+
return `<div style="padding:24px 32px;border-top:1px solid {{theme.colors.border}};font-size:13px;line-height:1.6;color:{{theme.colors.footer}};">
|
|
64
|
+
<p style="margin:0;">{{t.helloWorld.footer}}</p>
|
|
65
|
+
</div>
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
function helloWorldHtml() {
|
|
69
|
+
return `<div style="padding:32px;">
|
|
70
|
+
<p style="margin:0 0 16px;font-size:12px;letter-spacing:0.12em;text-transform:uppercase;color:{{theme.colors.primary}};">
|
|
71
|
+
{{t.helloWorld.eyebrow}}
|
|
72
|
+
</p>
|
|
73
|
+
<h1 style="margin:0 0 16px;font-size:32px;line-height:1.15;color:{{theme.colors.text}};">
|
|
74
|
+
{{t.helloWorld.title}}
|
|
75
|
+
</h1>
|
|
76
|
+
<p style="margin:0 0 16px;font-size:16px;line-height:1.7;color:{{theme.colors.muted}};">
|
|
77
|
+
Hello {{userName}}. {{t.helloWorld.intro}}
|
|
78
|
+
</p>
|
|
79
|
+
<p style="margin:0 0 24px;font-size:16px;line-height:1.7;color:{{theme.colors.muted}};">
|
|
80
|
+
{{t.helloWorld.body}}
|
|
81
|
+
</p>
|
|
82
|
+
<a
|
|
83
|
+
href="{{previewUrl}}"
|
|
84
|
+
style="display:inline-block;padding:14px 18px;border-radius:999px;background:{{theme.colors.primary}};color:{{theme.colors.primaryText}};font-weight:600;text-decoration:none;"
|
|
85
|
+
>
|
|
86
|
+
{{t.helloWorld.cta}}
|
|
87
|
+
</a>
|
|
88
|
+
</div>
|
|
89
|
+
{{> footer}}
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
function helloWorldSubject() {
|
|
93
|
+
return `{{t.helloWorld.subject}}
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
function helloWorldText() {
|
|
97
|
+
return `{{t.helloWorld.title}}
|
|
98
|
+
|
|
99
|
+
Hello {{userName}}.
|
|
100
|
+
|
|
101
|
+
{{t.helloWorld.intro}}
|
|
102
|
+
|
|
103
|
+
{{t.helloWorld.body}}
|
|
104
|
+
|
|
105
|
+
{{t.helloWorld.cta}}: {{previewUrl}}
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
async function ensureFile(path, content) {
|
|
109
|
+
await mkdir(dirname(path), { recursive: true });
|
|
110
|
+
await writeFile(path, content, 'utf8');
|
|
111
|
+
}
|
|
112
|
+
async function updateJsonConfig(configDir, emailDir) {
|
|
113
|
+
const configPath = join(configDir, 'pikku.config.json');
|
|
114
|
+
if (!existsSync(configPath)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const raw = await readFile(configPath, 'utf8');
|
|
118
|
+
const parsed = JSON.parse(raw);
|
|
119
|
+
if (typeof parsed.emailTemplatesDir === 'string' && parsed.emailTemplatesDir) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
parsed.emailTemplatesDir = emailDir;
|
|
123
|
+
await writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
export const pikkuEmailsInit = pikkuSessionlessFunc({
|
|
127
|
+
func: async ({ logger, config }, input) => {
|
|
128
|
+
const force = input?.force;
|
|
129
|
+
const configuredEmailDir = config.emailTemplatesDir ?? join(config.rootDir, DEFAULT_EMAIL_DIR);
|
|
130
|
+
const emailDir = config.emailTemplatesDir && config.emailTemplatesDir.length > 0
|
|
131
|
+
? config.emailTemplatesDir
|
|
132
|
+
: join(config.rootDir, DEFAULT_EMAIL_DIR);
|
|
133
|
+
const files = [
|
|
134
|
+
[join(emailDir, 'theme.json'), `${JSON.stringify(DEFAULT_THEME, null, 2)}\n`],
|
|
135
|
+
[join(emailDir, 'locales', 'en.json'), `${JSON.stringify(DEFAULT_EN_LOCALE, null, 2)}\n`],
|
|
136
|
+
[join(emailDir, 'locales', 'de.json'), `${JSON.stringify(DEFAULT_DE_LOCALE, null, 2)}\n`],
|
|
137
|
+
[join(emailDir, 'partials', 'layout.html'), layoutTemplate()],
|
|
138
|
+
[join(emailDir, 'partials', 'footer.html'), footerPartial()],
|
|
139
|
+
[join(emailDir, 'templates', 'hello-world.html'), helloWorldHtml()],
|
|
140
|
+
[join(emailDir, 'templates', 'hello-world.subject.txt'), helloWorldSubject()],
|
|
141
|
+
[join(emailDir, 'templates', 'hello-world.text.txt'), helloWorldText()],
|
|
142
|
+
];
|
|
143
|
+
const existing = !force
|
|
144
|
+
? files.map(([path]) => path).filter((path) => existsSync(path))
|
|
145
|
+
: [];
|
|
146
|
+
if (existing.length > 0) {
|
|
147
|
+
logger.error(`Email scaffold already exists at ${existing[0]}. Use --force to overwrite.`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
await Promise.all(files.map(([path, content]) => ensureFile(path, content)));
|
|
151
|
+
const configUpdated = !config.emailTemplatesDir &&
|
|
152
|
+
(await updateJsonConfig(config.configDir, DEFAULT_EMAIL_DIR));
|
|
153
|
+
if (!config.emailTemplatesDir && !configUpdated) {
|
|
154
|
+
logger.warn('Unable to auto-update pikku.config.json. Add "emailTemplatesDir": "emails" manually.');
|
|
155
|
+
}
|
|
156
|
+
await generateEmailsArtifacts(logger, {
|
|
157
|
+
emailTemplatesDir: emailDir,
|
|
158
|
+
outDir: config.outDir,
|
|
159
|
+
});
|
|
160
|
+
logger.info(`Email templates initialized at ${configuredEmailDir}`);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
3
4
|
import { register } from 'tsx/esm/api';
|
|
4
5
|
let tsxRegistered = false;
|
|
6
|
+
function isBunRuntime() {
|
|
7
|
+
return typeof globalThis.Bun !== 'undefined';
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
10
|
* Globally register tsx so subsequent `import()` calls go through Node's
|
|
7
11
|
* normal resolver — yielding ONE module graph (and one shared pikkuState),
|
|
@@ -13,22 +17,27 @@ let tsxRegistered = false;
|
|
|
13
17
|
* module state (e.g. `pikkuState` registrations from wireHTTPRoutes).
|
|
14
18
|
*/
|
|
15
19
|
function ensureTsxRegistered() {
|
|
20
|
+
if (isBunRuntime())
|
|
21
|
+
return;
|
|
16
22
|
if (tsxRegistered)
|
|
17
23
|
return;
|
|
18
24
|
register();
|
|
19
25
|
tsxRegistered = true;
|
|
20
26
|
}
|
|
27
|
+
async function importUserPath(filePath) {
|
|
28
|
+
return import(pathToFileURL(filePath).href);
|
|
29
|
+
}
|
|
21
30
|
/**
|
|
22
31
|
* Load the generated `pikku-bootstrap.gen.{ts,js}` from the user's project,
|
|
23
32
|
* which in turn pulls in all wiring/meta files so they register into
|
|
24
33
|
* `pikkuState`.
|
|
25
34
|
*/
|
|
26
35
|
export async function loadUserBootstrap(pikkuDir) {
|
|
27
|
-
ensureTsxRegistered();
|
|
28
36
|
const bootstrapTs = join(pikkuDir, 'pikku-bootstrap.gen.ts');
|
|
29
37
|
const bootstrapJs = join(pikkuDir, 'pikku-bootstrap.gen.js');
|
|
30
38
|
const bootstrapPath = existsSync(bootstrapTs) ? bootstrapTs : bootstrapJs;
|
|
31
|
-
|
|
39
|
+
ensureTsxRegistered();
|
|
40
|
+
await importUserPath(bootstrapPath);
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* Import a user-source TypeScript file (e.g. their config or services
|
|
@@ -36,5 +45,5 @@ export async function loadUserBootstrap(pikkuDir) {
|
|
|
36
45
|
*/
|
|
37
46
|
export async function loadUserModule(filePath) {
|
|
38
47
|
ensureTsxRegistered();
|
|
39
|
-
return
|
|
48
|
+
return importUserPath(filePath);
|
|
40
49
|
}
|
|
@@ -4,21 +4,24 @@ import { pikkuDevReloader } from '@pikku/core/dev';
|
|
|
4
4
|
export const watch = pikkuSessionlessFunc({
|
|
5
5
|
remote: true,
|
|
6
6
|
func: async ({ logger, config }, { hmr }, { rpc }) => {
|
|
7
|
+
const watchDirectories = [
|
|
8
|
+
...new Set([config.emailTemplatesDir, ...config.srcDirectories].filter(Boolean)),
|
|
9
|
+
];
|
|
7
10
|
if (hmr) {
|
|
8
11
|
await pikkuDevReloader({
|
|
9
|
-
srcDirectories:
|
|
12
|
+
srcDirectories: watchDirectories,
|
|
10
13
|
logger,
|
|
11
14
|
});
|
|
12
15
|
}
|
|
13
|
-
const configWatcher = chokidar.watch(
|
|
16
|
+
const configWatcher = chokidar.watch(watchDirectories, {
|
|
14
17
|
ignoreInitial: true,
|
|
15
18
|
ignored: /.*\.gen\.tsx?/,
|
|
16
19
|
});
|
|
17
20
|
let watcher = new chokidar.FSWatcher({});
|
|
18
21
|
const generatorWatcher = () => {
|
|
19
22
|
watcher.close();
|
|
20
|
-
logger.info(`• Watching directories: \n - ${
|
|
21
|
-
watcher = chokidar.watch(
|
|
23
|
+
logger.info(`• Watching directories: \n - ${watchDirectories.join('\n - ')}`);
|
|
24
|
+
watcher = chokidar.watch(watchDirectories, {
|
|
22
25
|
ignoreInitial: true,
|
|
23
26
|
ignored: /.*\.gen\.ts/,
|
|
24
27
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { renderWorkspaceValidate } from '../validate/workspace-validate.js';
|
|
2
|
+
export declare const workspaceValidate: import("#pikku").PikkuFunctionConfig<Record<string, never>, {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
root: string;
|
|
5
|
+
findings: {
|
|
6
|
+
id: string;
|
|
7
|
+
severity: "info" | "warn" | "error";
|
|
8
|
+
message: string;
|
|
9
|
+
path: string;
|
|
10
|
+
fixHint: string;
|
|
11
|
+
}[];
|
|
12
|
+
}, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<Record<string, never>, {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
root: string;
|
|
15
|
+
findings: {
|
|
16
|
+
id: string;
|
|
17
|
+
severity: "info" | "warn" | "error";
|
|
18
|
+
message: string;
|
|
19
|
+
path: string;
|
|
20
|
+
fixHint: string;
|
|
21
|
+
}[];
|
|
22
|
+
}, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<Record<string, never>, {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
root: string;
|
|
25
|
+
findings: {
|
|
26
|
+
id: string;
|
|
27
|
+
severity: "info" | "warn" | "error";
|
|
28
|
+
message: string;
|
|
29
|
+
path: string;
|
|
30
|
+
fixHint: string;
|
|
31
|
+
}[];
|
|
32
|
+
}, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
|
|
33
|
+
export { renderWorkspaceValidate };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
+
import { WorkspaceValidateInput, WorkspaceValidateOutput, renderWorkspaceValidate, runWorkspaceValidate, } from '../validate/workspace-validate.js';
|
|
3
|
+
export const workspaceValidate = pikkuSessionlessFunc({
|
|
4
|
+
description: 'Check the current Pikku workspace structure for compatibility. Prints all missing or misconfigured items with fix hints.',
|
|
5
|
+
input: WorkspaceValidateInput,
|
|
6
|
+
output: WorkspaceValidateOutput,
|
|
7
|
+
func: async () => runWorkspaceValidate(),
|
|
8
|
+
});
|
|
9
|
+
export { renderWorkspaceValidate };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { KyselyPlugin } from 'kysely';
|
|
2
|
+
export type ColumnKind = 'date' | 'bool' | 'json';
|
|
3
|
+
export type CoercionMap = Record<string, Record<string, ColumnKind>>;
|
|
4
|
+
export interface CreateCoercionPluginOptions {
|
|
5
|
+
map: CoercionMap;
|
|
6
|
+
}
|
|
7
|
+
export declare function createCoercionPlugin(options: CreateCoercionPluginOptions): KyselyPlugin;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
function fromDb(value, kind) {
|
|
2
|
+
if (value == null)
|
|
3
|
+
return value;
|
|
4
|
+
switch (kind) {
|
|
5
|
+
case 'date':
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
const d = new Date(value);
|
|
8
|
+
return Number.isNaN(d.getTime()) ? value : d;
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
case 'bool':
|
|
12
|
+
if (typeof value === 'number')
|
|
13
|
+
return value !== 0;
|
|
14
|
+
if (typeof value === 'bigint')
|
|
15
|
+
return value !== 0n;
|
|
16
|
+
return value;
|
|
17
|
+
case 'json':
|
|
18
|
+
if (typeof value !== 'string')
|
|
19
|
+
return value;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(value);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function snakeToCamel(name) {
|
|
29
|
+
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
30
|
+
}
|
|
31
|
+
function buildGlobalMap(map) {
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [table, tbl] of Object.entries(map)) {
|
|
34
|
+
for (const [col, kind] of Object.entries(tbl)) {
|
|
35
|
+
out[`${table}.${col}`] = kind;
|
|
36
|
+
out[`${table}.${snakeToCamel(col)}`] = kind;
|
|
37
|
+
out[col] = kind;
|
|
38
|
+
out[snakeToCamel(col)] = kind;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function collectQueryTables(node, out) {
|
|
44
|
+
if (!node || typeof node !== 'object')
|
|
45
|
+
return;
|
|
46
|
+
const op = node;
|
|
47
|
+
if (op.kind === 'TableNode') {
|
|
48
|
+
const tableName = op.table?.identifier?.name;
|
|
49
|
+
if (typeof tableName === 'string' && tableName.length > 0)
|
|
50
|
+
out.add(tableName);
|
|
51
|
+
}
|
|
52
|
+
for (const value of Object.values(node)) {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
for (const item of value)
|
|
55
|
+
collectQueryTables(item, out);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
collectQueryTables(value, out);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function lookupKind(globalMap, tables, col) {
|
|
63
|
+
let matchedKind;
|
|
64
|
+
for (const table of tables) {
|
|
65
|
+
const kind = globalMap[`${table}.${col}`];
|
|
66
|
+
if (!kind)
|
|
67
|
+
continue;
|
|
68
|
+
if (matchedKind && matchedKind !== kind)
|
|
69
|
+
return globalMap[col];
|
|
70
|
+
matchedKind = kind;
|
|
71
|
+
}
|
|
72
|
+
return matchedKind ?? globalMap[col];
|
|
73
|
+
}
|
|
74
|
+
export function createCoercionPlugin(options) {
|
|
75
|
+
const globalMap = buildGlobalMap(options.map);
|
|
76
|
+
const queryTables = new WeakMap();
|
|
77
|
+
return {
|
|
78
|
+
transformQuery(args) {
|
|
79
|
+
const tables = new Set();
|
|
80
|
+
collectQueryTables(args.node, tables);
|
|
81
|
+
queryTables.set(args.queryId, [...tables]);
|
|
82
|
+
return args.node;
|
|
83
|
+
},
|
|
84
|
+
async transformResult(args) {
|
|
85
|
+
const tables = queryTables.get(args.queryId) ?? [];
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const row of args.result.rows) {
|
|
88
|
+
const next = { ...row };
|
|
89
|
+
for (const [col, val] of Object.entries(row)) {
|
|
90
|
+
const kind = lookupKind(globalMap, tables, col);
|
|
91
|
+
if (kind)
|
|
92
|
+
next[col] = fromDb(val, kind);
|
|
93
|
+
}
|
|
94
|
+
out.push(next);
|
|
95
|
+
}
|
|
96
|
+
return { ...args.result, rows: out };
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -32,8 +32,8 @@ export interface MigrateAndCodegenOutcome {
|
|
|
32
32
|
* Run the migrate routine (open → tracking-table → drift-check → apply →
|
|
33
33
|
* codegen → close). Used by both `pikku db migrate` and `pikku dev` boot.
|
|
34
34
|
*/
|
|
35
|
-
export declare function migrateAndCodegen(resolved: ResolvedLocalDb): MigrateAndCodegenOutcome
|
|
36
|
-
export declare function seed(resolved: ResolvedLocalDb): SeedResult
|
|
35
|
+
export declare function migrateAndCodegen(resolved: ResolvedLocalDb): Promise<MigrateAndCodegenOutcome>;
|
|
36
|
+
export declare function seed(resolved: ResolvedLocalDb): Promise<SeedResult>;
|
|
37
37
|
/**
|
|
38
38
|
* Delete the dev DB file. Refuses if NODE_ENV is 'production' or the
|
|
39
39
|
* resolved file lives outside the project root (defensive against
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { DatabaseSync } from 'node:sqlite';
|
|
2
1
|
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
2
|
import { resolve, isAbsolute, relative, dirname, join } from 'node:path';
|
|
4
|
-
import { createNodeSqliteKysely, createCoercionPlugin, } from '@pikku/kysely-node-sqlite';
|
|
5
3
|
import { migrate } from './sql-migrator.js';
|
|
6
4
|
import { generateSchemaTypes } from './sqlite-codegen.js';
|
|
7
5
|
import { generateZodTypes } from './zod-codegen.js';
|
|
8
6
|
import { seed as runSeed } from './seed.js';
|
|
7
|
+
import { createCoercionPlugin } from './coercion-plugin.js';
|
|
8
|
+
import { createSqliteKysely } from './sqlite-kysely.js';
|
|
9
|
+
import { loadSqliteRuntime } from './sqlite-runtime.js';
|
|
9
10
|
/**
|
|
10
11
|
* Resolve a DevDbConfig into absolute paths.
|
|
11
12
|
* - dbFile lives under runtimeDir (default: <rootDir>/.pikku-runtime)
|
|
@@ -37,9 +38,10 @@ function resolveAgainst(root, p) {
|
|
|
37
38
|
* Run the migrate routine (open → tracking-table → drift-check → apply →
|
|
38
39
|
* codegen → close). Used by both `pikku db migrate` and `pikku dev` boot.
|
|
39
40
|
*/
|
|
40
|
-
export function migrateAndCodegen(resolved) {
|
|
41
|
+
export async function migrateAndCodegen(resolved) {
|
|
41
42
|
mkdirSync(dirname(resolved.dbFile), { recursive: true });
|
|
42
|
-
const
|
|
43
|
+
const runtime = await loadSqliteRuntime();
|
|
44
|
+
const db = runtime.open(resolved.dbFile);
|
|
43
45
|
try {
|
|
44
46
|
const migrateResult = migrate(db, resolved.migrationsDir);
|
|
45
47
|
const codegenResult = generateSchemaTypes(db, {
|
|
@@ -58,8 +60,9 @@ export function migrateAndCodegen(resolved) {
|
|
|
58
60
|
db.close();
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
|
-
export function seed(resolved) {
|
|
62
|
-
const
|
|
63
|
+
export async function seed(resolved) {
|
|
64
|
+
const runtime = await loadSqliteRuntime();
|
|
65
|
+
const db = runtime.open(resolved.dbFile);
|
|
63
66
|
try {
|
|
64
67
|
return runSeed(db, resolved.seedFile);
|
|
65
68
|
}
|
|
@@ -90,6 +93,8 @@ export function reset(resolved, rootDir) {
|
|
|
90
93
|
* Wires the coercion plugin when db/coercion.gen.ts exists.
|
|
91
94
|
*/
|
|
92
95
|
export async function createKysely(resolved) {
|
|
96
|
+
mkdirSync(dirname(resolved.dbFile), { recursive: true });
|
|
97
|
+
const runtime = await loadSqliteRuntime();
|
|
93
98
|
let coercionMap;
|
|
94
99
|
try {
|
|
95
100
|
const mod = await import(resolved.coercionFile);
|
|
@@ -98,8 +103,8 @@ export async function createKysely(resolved) {
|
|
|
98
103
|
catch {
|
|
99
104
|
// coercion.gen.ts not yet generated — run `pikku db migrate` first
|
|
100
105
|
}
|
|
101
|
-
return
|
|
102
|
-
|
|
106
|
+
return createSqliteKysely({
|
|
107
|
+
db: runtime.open(resolved.dbFile),
|
|
103
108
|
camelCase: resolved.camelCase,
|
|
104
109
|
plugins: coercionMap ? [createCoercionPlugin({ map: coercionMap })] : [],
|
|
105
110
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SyncSqliteDatabase } from './sqlite-runtime.js';
|
|
2
2
|
export interface SeedResult {
|
|
3
3
|
applied: boolean;
|
|
4
4
|
bytes: number;
|
|
@@ -8,4 +8,4 @@ export interface SeedResult {
|
|
|
8
8
|
* (e.g. `INSERT OR IGNORE`, upserts). Returns `applied: false` if the file
|
|
9
9
|
* doesn't exist; throws on SQL errors.
|
|
10
10
|
*/
|
|
11
|
-
export declare function seed(db:
|
|
11
|
+
export declare function seed(db: SyncSqliteDatabase, seedFile: string): SeedResult;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SyncSqliteDatabase } from './sqlite-runtime.js';
|
|
2
2
|
export declare class MigrationDriftError extends Error {
|
|
3
3
|
readonly file: string;
|
|
4
4
|
readonly recordedHash: string;
|
|
@@ -18,9 +18,9 @@ export interface MigrateResult {
|
|
|
18
18
|
* The same bytes that get hashed are the bytes passed to db.exec — no
|
|
19
19
|
* splitting, trimming, or normalization. See docs/dev-builtin-sqlite.md.
|
|
20
20
|
*/
|
|
21
|
-
export declare function migrate(db:
|
|
21
|
+
export declare function migrate(db: SyncSqliteDatabase, migrationsDir: string): MigrateResult;
|
|
22
22
|
/**
|
|
23
23
|
* Wipe the tracking table. Used by `pikku db reset` after the DB file is
|
|
24
24
|
* removed (calling this on its own does NOT drop user tables).
|
|
25
25
|
*/
|
|
26
|
-
export declare function dropTrackingTable(db:
|
|
26
|
+
export declare function dropTrackingTable(db: SyncSqliteDatabase): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { ColumnKind } from './coercion-plugin.js';
|
|
2
|
+
import type { SyncSqliteDatabase } from './sqlite-runtime.js';
|
|
3
3
|
/** Internal annotation: column kind plus an optional TS type string (for @json). */
|
|
4
4
|
interface ColAnnotation {
|
|
5
5
|
kind: ColumnKind;
|
|
@@ -41,5 +41,5 @@ export interface CodegenResult {
|
|
|
41
41
|
*
|
|
42
42
|
* Returns `written: false` if the on-disk file already matches.
|
|
43
43
|
*/
|
|
44
|
-
export declare function generateSchemaTypes(db:
|
|
44
|
+
export declare function generateSchemaTypes(db: SyncSqliteDatabase, options: CodegenOptions): CodegenResult;
|
|
45
45
|
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Kysely, type KyselyPlugin } from 'kysely';
|
|
2
|
+
import type { SyncSqliteDatabase } from './sqlite-runtime.js';
|
|
3
|
+
export interface CreateSqliteKyselyOptions {
|
|
4
|
+
db: SyncSqliteDatabase;
|
|
5
|
+
camelCase?: boolean;
|
|
6
|
+
plugins?: KyselyPlugin[];
|
|
7
|
+
}
|
|
8
|
+
export declare function createSqliteKysely<DB>(options: CreateSqliteKyselyOptions): Kysely<DB>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Kysely, SqliteDialect, CamelCasePlugin, } from 'kysely';
|
|
2
|
+
function coerce(v) {
|
|
3
|
+
if (v === null || v === undefined)
|
|
4
|
+
return null;
|
|
5
|
+
if (typeof v === 'boolean')
|
|
6
|
+
return v ? 1 : 0;
|
|
7
|
+
if (v instanceof Date)
|
|
8
|
+
return v.toISOString();
|
|
9
|
+
if (v instanceof Uint8Array)
|
|
10
|
+
return v;
|
|
11
|
+
if (typeof v === 'object')
|
|
12
|
+
return JSON.stringify(v);
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
class RuntimeSqliteStatement {
|
|
16
|
+
stmt;
|
|
17
|
+
reader;
|
|
18
|
+
constructor(stmt) {
|
|
19
|
+
this.stmt = stmt;
|
|
20
|
+
this.reader = Boolean(stmt.reader);
|
|
21
|
+
}
|
|
22
|
+
all(parameters) {
|
|
23
|
+
return this.stmt.all(...parameters.map(coerce));
|
|
24
|
+
}
|
|
25
|
+
*iterate(parameters) {
|
|
26
|
+
for (const row of this.stmt.iterate(...parameters.map(coerce))) {
|
|
27
|
+
yield row;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
run(parameters) {
|
|
31
|
+
const result = this.stmt.run(...parameters.map(coerce));
|
|
32
|
+
return {
|
|
33
|
+
changes: result.changes,
|
|
34
|
+
lastInsertRowid: result.lastInsertRowid,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class RuntimeSqliteDatabase {
|
|
39
|
+
db;
|
|
40
|
+
constructor(db) {
|
|
41
|
+
this.db = db;
|
|
42
|
+
}
|
|
43
|
+
prepare(sql) {
|
|
44
|
+
return new RuntimeSqliteStatement(this.db.prepare(sql));
|
|
45
|
+
}
|
|
46
|
+
close() {
|
|
47
|
+
this.db.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function createSqliteKysely(options) {
|
|
51
|
+
const plugins = [];
|
|
52
|
+
if (options.camelCase ?? true)
|
|
53
|
+
plugins.push(new CamelCasePlugin());
|
|
54
|
+
if (options.plugins)
|
|
55
|
+
plugins.push(...options.plugins);
|
|
56
|
+
return new Kysely({
|
|
57
|
+
dialect: new SqliteDialect({
|
|
58
|
+
database: new RuntimeSqliteDatabase(options.db),
|
|
59
|
+
}),
|
|
60
|
+
plugins,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
class BunSqliteStatement {
|
|
3
|
+
stmt;
|
|
4
|
+
reader;
|
|
5
|
+
constructor(stmt, reader) {
|
|
6
|
+
this.stmt = stmt;
|
|
7
|
+
this.reader = reader;
|
|
8
|
+
}
|
|
9
|
+
all(...parameters) {
|
|
10
|
+
return this.stmt.all(...parameters);
|
|
11
|
+
}
|
|
12
|
+
get(...parameters) {
|
|
13
|
+
return this.stmt.get(...parameters) ?? null;
|
|
14
|
+
}
|
|
15
|
+
iterate(...parameters) {
|
|
16
|
+
return this.stmt.iterate(...parameters);
|
|
17
|
+
}
|
|
18
|
+
run(...parameters) {
|
|
19
|
+
const result = this.stmt.run(...parameters);
|
|
20
|
+
return {
|
|
21
|
+
changes: result.changes,
|
|
22
|
+
lastInsertRowid: result.lastInsertRowid,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class BunSqliteDatabase {
|
|
27
|
+
db;
|
|
28
|
+
constructor(db) {
|
|
29
|
+
this.db = db;
|
|
30
|
+
}
|
|
31
|
+
exec(sql) {
|
|
32
|
+
this.db.exec(sql);
|
|
33
|
+
}
|
|
34
|
+
prepare(sql) {
|
|
35
|
+
return new BunSqliteStatement(this.db.prepare(sql), isReaderSql(sql));
|
|
36
|
+
}
|
|
37
|
+
close() {
|
|
38
|
+
this.db.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isReaderSql(sql) {
|
|
42
|
+
const normalized = sql.trimStart().toUpperCase();
|
|
43
|
+
return (normalized.startsWith('SELECT') ||
|
|
44
|
+
normalized.startsWith('WITH') ||
|
|
45
|
+
normalized.startsWith('PRAGMA') ||
|
|
46
|
+
normalized.startsWith('EXPLAIN'));
|
|
47
|
+
}
|
|
48
|
+
export const bunSqliteRuntime = {
|
|
49
|
+
open(filename) {
|
|
50
|
+
return new BunSqliteDatabase(new Database(filename));
|
|
51
|
+
},
|
|
52
|
+
};
|