@kuratchi/js 0.0.21 → 0.0.22

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.
@@ -0,0 +1,175 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ export function buildDesktopManifest(opts) {
4
+ const { projectDir, workerFile, desktopConfig, ormDatabases } = opts;
5
+ if (!desktopConfig)
6
+ return null;
7
+ const wranglerConfig = readDesktopWranglerConfig(projectDir);
8
+ const projectName = path.basename(projectDir);
9
+ const appName = desktopConfig.appName ?? projectName;
10
+ const appId = desktopConfig.appId ?? `dev.kuratchi.${projectName.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`;
11
+ const assetsRoot = resolveAssetsRoot(projectDir);
12
+ const requestedRemoteBindings = desktopConfig.remoteBindings.length > 0
13
+ ? desktopConfig.remoteBindings
14
+ : ormDatabases
15
+ .filter((entry) => entry.type === 'd1' && entry.remote)
16
+ .map((entry) => ({ binding: entry.binding, type: 'd1', remote: true }));
17
+ const remoteBindings = requestedRemoteBindings.map((binding) => {
18
+ if (binding.type === 'd1') {
19
+ const wranglerBinding = wranglerConfig.d1Databases.find((entry) => entry.binding === binding.binding && entry.remote);
20
+ if (!wranglerBinding) {
21
+ throw new Error(`Desktop manifest could not resolve remote D1 binding "${binding.binding}" from wrangler.jsonc.`);
22
+ }
23
+ return {
24
+ binding: binding.binding,
25
+ type: 'd1',
26
+ remote: true,
27
+ databaseId: wranglerBinding.databaseId,
28
+ databaseName: wranglerBinding.databaseName,
29
+ };
30
+ }
31
+ const wranglerBinding = wranglerConfig.r2Buckets.find((entry) => entry.binding === binding.binding && entry.remote);
32
+ if (!wranglerBinding) {
33
+ throw new Error(`Desktop manifest could not resolve remote R2 binding "${binding.binding}" from wrangler.jsonc.`);
34
+ }
35
+ return {
36
+ binding: binding.binding,
37
+ type: 'r2',
38
+ remote: true,
39
+ bucketName: wranglerBinding.bucketName,
40
+ };
41
+ });
42
+ return {
43
+ formatVersion: 1,
44
+ generatedAt: new Date().toISOString(),
45
+ projectDir,
46
+ app: {
47
+ name: appName,
48
+ id: appId,
49
+ initialPath: desktopConfig.initialPath || '/',
50
+ window: {
51
+ title: desktopConfig.windowTitle ?? appName,
52
+ width: desktopConfig.windowWidth,
53
+ height: desktopConfig.windowHeight,
54
+ },
55
+ },
56
+ runtime: {
57
+ workerEntrypoint: workerFile,
58
+ assetsRoot,
59
+ compatibilityDate: wranglerConfig.compatibilityDate,
60
+ compatibilityFlags: wranglerConfig.compatibilityFlags,
61
+ cloudflareAccountId: wranglerConfig.cloudflareAccountId,
62
+ },
63
+ bindings: {
64
+ desktop: {
65
+ notifications: desktopConfig.bindings.notifications,
66
+ files: desktopConfig.bindings.files,
67
+ },
68
+ remote: remoteBindings,
69
+ },
70
+ };
71
+ }
72
+ export function writeDesktopManifest(projectDir, manifest) {
73
+ const outFile = path.join(projectDir, '.kuratchi', 'desktop.manifest.json');
74
+ const next = `${JSON.stringify(manifest, null, 2)}\n`;
75
+ if (fs.existsSync(outFile)) {
76
+ const current = fs.readFileSync(outFile, 'utf-8');
77
+ if (current === next)
78
+ return outFile;
79
+ }
80
+ fs.writeFileSync(outFile, next, 'utf-8');
81
+ return outFile;
82
+ }
83
+ function resolveAssetsRoot(projectDir) {
84
+ const publicDir = path.join(projectDir, '.kuratchi', 'public');
85
+ if (fs.existsSync(publicDir))
86
+ return publicDir;
87
+ const srcAssets = path.join(projectDir, 'src', 'assets');
88
+ if (fs.existsSync(srcAssets))
89
+ return srcAssets;
90
+ return null;
91
+ }
92
+ function readDesktopWranglerConfig(projectDir) {
93
+ const configPath = ['wrangler.jsonc', 'wrangler.json']
94
+ .map((name) => path.join(projectDir, name))
95
+ .find((candidate) => fs.existsSync(candidate));
96
+ if (!configPath) {
97
+ throw new Error('Desktop runtime requires wrangler.jsonc or wrangler.json for compatibility settings and remote bindings.');
98
+ }
99
+ const rawConfig = fs.readFileSync(configPath, 'utf-8');
100
+ const parsed = JSON.parse(stripJsonComments(rawConfig));
101
+ if (!parsed.compatibility_date) {
102
+ throw new Error(`Missing compatibility_date in ${path.basename(configPath)}.`);
103
+ }
104
+ return {
105
+ compatibilityDate: parsed.compatibility_date,
106
+ compatibilityFlags: Array.isArray(parsed.compatibility_flags) ? parsed.compatibility_flags : [],
107
+ cloudflareAccountId: typeof parsed.account_id === 'string' && parsed.account_id.trim().length > 0
108
+ ? parsed.account_id.trim()
109
+ : null,
110
+ d1Databases: Array.isArray(parsed.d1_databases)
111
+ ? parsed.d1_databases
112
+ .filter((entry) => entry.binding && entry.database_id)
113
+ .map((entry) => ({
114
+ binding: entry.binding,
115
+ databaseId: entry.database_id,
116
+ databaseName: entry.database_name ?? null,
117
+ remote: entry.remote !== false,
118
+ }))
119
+ : [],
120
+ r2Buckets: Array.isArray(parsed.r2_buckets)
121
+ ? parsed.r2_buckets
122
+ .filter((entry) => entry.binding && entry.bucket_name)
123
+ .map((entry) => ({
124
+ binding: entry.binding,
125
+ bucketName: entry.bucket_name,
126
+ remote: entry.remote !== false,
127
+ }))
128
+ : [],
129
+ };
130
+ }
131
+ function stripJsonComments(content) {
132
+ let result = '';
133
+ let i = 0;
134
+ let inString = false;
135
+ let stringChar = '';
136
+ while (i < content.length) {
137
+ const ch = content[i];
138
+ const next = content[i + 1];
139
+ if (inString) {
140
+ result += ch;
141
+ if (ch === '\\' && i + 1 < content.length) {
142
+ result += next;
143
+ i += 2;
144
+ continue;
145
+ }
146
+ if (ch === stringChar) {
147
+ inString = false;
148
+ }
149
+ i++;
150
+ continue;
151
+ }
152
+ if (ch === '"' || ch === '\'') {
153
+ inString = true;
154
+ stringChar = ch;
155
+ result += ch;
156
+ i++;
157
+ continue;
158
+ }
159
+ if (ch === '/' && next === '/') {
160
+ while (i < content.length && content[i] !== '\n')
161
+ i++;
162
+ continue;
163
+ }
164
+ if (ch === '/' && next === '*') {
165
+ i += 2;
166
+ while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/'))
167
+ i++;
168
+ i += 2;
169
+ continue;
170
+ }
171
+ result += ch;
172
+ i++;
173
+ }
174
+ return result;
175
+ }
@@ -1,6 +1,10 @@
1
- import { type DoConfigEntry, type DoHandlerEntry, type OrmDatabaseEntry } from './compiler-shared.js';
2
- export declare function discoverDurableObjects(srcDir: string, configDoEntries: DoConfigEntry[], ormDatabases: OrmDatabaseEntry[]): {
3
- config: DoConfigEntry[];
1
+ import { type DoHandlerEntry } from './compiler-shared.js';
2
+ export declare function discoverDurableObjects(srcDir: string): {
3
+ config: {
4
+ binding: string;
5
+ className: string;
6
+ files?: string[];
7
+ }[];
4
8
  handlers: DoHandlerEntry[];
5
9
  };
6
10
  export declare function generateHandlerProxy(handler: DoHandlerEntry, opts: {
@@ -3,27 +3,14 @@ import * as path from 'node:path';
3
3
  import * as ts from 'typescript';
4
4
  import { toSafeIdentifier, } from './compiler-shared.js';
5
5
  import { discoverFilesWithExtensions, discoverFilesWithSuffix } from './convention-discovery.js';
6
- export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
6
+ export function discoverDurableObjects(srcDir) {
7
7
  const serverDir = path.join(srcDir, 'server');
8
8
  const legacyDir = path.join(srcDir, 'durable-objects');
9
9
  const serverDoFiles = discoverFilesWithSuffix(serverDir, '.do.ts');
10
10
  const legacyDoFiles = discoverFilesWithSuffix(legacyDir, '.ts');
11
11
  const discoveredFiles = Array.from(new Set([...serverDoFiles, ...legacyDoFiles]));
12
12
  if (discoveredFiles.length === 0) {
13
- return { config: configDoEntries, handlers: [] };
14
- }
15
- const bindings = new Set(configDoEntries.map((d) => d.binding));
16
- const fileToBinding = new Map();
17
- for (const entry of configDoEntries) {
18
- for (const rawFile of entry.files ?? []) {
19
- const normalized = rawFile.trim().replace(/^\.?[\\/]/, '').replace(/\\/g, '/').toLowerCase();
20
- if (!normalized)
21
- continue;
22
- fileToBinding.set(normalized, entry.binding);
23
- const base = path.basename(normalized);
24
- if (!fileToBinding.has(base))
25
- fileToBinding.set(base, entry.binding);
26
- }
13
+ return { config: [], handlers: [] };
27
14
  }
28
15
  const handlers = [];
29
16
  const handlerIdToAbsPath = new Map();
@@ -48,28 +35,17 @@ export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
48
35
  continue;
49
36
  // Binding resolution:
50
37
  // 1) explicit static binding declared in the class
51
- // 2) config-mapped file name
52
- // 3) if exactly one binding exists, infer it
38
+ // 2) derive from the handler file name
53
39
  let binding = null;
54
40
  const bindingMatch = source.match(/static\s+binding\s*=\s*['"](\w+)['"]/);
55
41
  if (bindingMatch) {
56
42
  binding = bindingMatch[1];
57
43
  }
58
44
  else {
59
- const normalizedFile = file.replace(/\\/g, '/').toLowerCase();
60
- const normalizedRelFromSrc = path.relative(srcDir, absPath).replace(/\\/g, '/').toLowerCase();
61
- binding = className ? (configDoEntries.find((entry) => entry.className === className)?.binding ?? null) : null;
62
- if (!binding) {
63
- binding = fileToBinding.get(normalizedRelFromSrc) ?? fileToBinding.get(normalizedFile) ?? null;
64
- }
65
- if (!binding && configDoEntries.length === 1) {
66
- binding = configDoEntries[0].binding;
67
- }
45
+ binding = deriveBindingFromFile(file);
68
46
  }
69
47
  if (!binding)
70
48
  continue;
71
- if (!bindings.has(binding))
72
- continue;
73
49
  const classMethods = className ? extractClassMethods(absPath, source, className) : [];
74
50
  const fileName = path
75
51
  .relative(absPath.startsWith(serverDir) ? serverDir : legacyDir, absPath)
@@ -103,17 +79,14 @@ export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
103
79
  }
104
80
  }
105
81
  // Build config entries from discovered handlers (de-duped by binding).
106
- // Prefer class name from the original config entry (e.g. from wrangler.jsonc).
82
+ // The handler source is authoritative; wrangler.jsonc is then synced from this.
107
83
  const discoveredConfigByBinding = new Map();
108
84
  for (const handler of handlers) {
109
- const configEntry = configDoEntries.find((e) => e.binding === handler.binding);
110
85
  const existing = discoveredConfigByBinding.get(handler.binding);
111
86
  if (!existing) {
112
87
  discoveredConfigByBinding.set(handler.binding, {
113
88
  binding: handler.binding,
114
- // Use config class name when available (authoritative, e.g. from wrangler.jsonc).
115
- className: configEntry?.className ?? handler.className ?? handler.binding,
116
- stubId: configEntry?.stubId,
89
+ className: handler.className ?? deriveClassNameFromFile(path.basename(handler.absPath)),
117
90
  files: [path.basename(handler.absPath)],
118
91
  });
119
92
  }
@@ -121,9 +94,32 @@ export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
121
94
  existing.files?.push(path.basename(handler.absPath));
122
95
  }
123
96
  }
124
- void ormDatabases;
125
97
  return { config: [...discoveredConfigByBinding.values()], handlers };
126
98
  }
99
+ function deriveBindingFromFile(fileName) {
100
+ const normalized = fileName
101
+ .replace(/\.(ts|tsx|js|mjs|cjs)$/i, '')
102
+ .replace(/\.do$/i, '')
103
+ .replace(/[^A-Za-z0-9]+/g, '_')
104
+ .replace(/^_+|_+$/g, '')
105
+ .toUpperCase();
106
+ if (!normalized)
107
+ return 'DURABLE_OBJECT';
108
+ return normalized.endsWith('_DO') ? normalized : `${normalized}_DO`;
109
+ }
110
+ function deriveClassNameFromFile(fileName) {
111
+ const normalized = fileName
112
+ .replace(/\.(ts|tsx|js|mjs|cjs)$/i, '')
113
+ .replace(/\.do$/i, '');
114
+ const base = normalized
115
+ .split(/[^A-Za-z0-9]+/)
116
+ .filter(Boolean)
117
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
118
+ .join('');
119
+ if (!base)
120
+ return 'DurableObjectHandler';
121
+ return base.endsWith('DO') ? base : `${base}DO`;
122
+ }
127
123
  // ---------------------------------------------------------------------------
128
124
  // TypeScript AST helpers
129
125
  // ---------------------------------------------------------------------------
@@ -7,7 +7,7 @@ import { compileAssets } from './asset-pipeline.js';
7
7
  import { compileApiRoute } from './api-route-pipeline.js';
8
8
  import { createClientModuleCompiler } from './client-module-pipeline.js';
9
9
  import { createComponentCompiler } from './component-pipeline.js';
10
- import { readAssetsPrefix, readAuthConfig, readDoConfig, readOrmConfig, readSecurityConfig, readUiConfigValues, readUiTheme, } from './config-reading.js';
10
+ import { readAssetsPrefix, readAuthConfig, readOrmConfig, readSecurityConfig, readUiConfigValues, readUiTheme, } from './config-reading.js';
11
11
  import { discoverContainerFiles, discoverConventionClassFiles, discoverWorkflowFiles, } from './convention-discovery.js';
12
12
  import { discoverDurableObjects, generateHandlerProxy } from './durable-object-pipeline.js';
13
13
  import { compileErrorPages } from './error-page-pipeline.js';
@@ -132,8 +132,7 @@ export async function compile(options) {
132
132
  // Read security config from kuratchi.config.ts
133
133
  const securityConfig = readSecurityConfig(projectDir);
134
134
  // Auto-discover Durable Objects from .do.ts files (config optional, only needed for stubId)
135
- const configDoEntries = readDoConfig(projectDir);
136
- const { config: doConfig, handlers: doHandlers } = discoverDurableObjects(srcDir, configDoEntries, ormDatabases);
135
+ const { config: doConfig, handlers: doHandlers } = discoverDurableObjects(srcDir);
137
136
  // Auto-discover convention-based worker class files (no config needed)
138
137
  const containerConfig = discoverContainerFiles(projectDir);
139
138
  const workflowConfig = discoverWorkflowFiles(projectDir);
@@ -1135,6 +1135,7 @@ export function parseFile(source, options = {}) {
1135
1135
  const actionFunctions = [];
1136
1136
  const pollFunctions = [];
1137
1137
  const dataGetQueries = [];
1138
+ let warnedLegacyActionAttrs = false;
1138
1139
  for (const tag of templateTags) {
1139
1140
  if (tag.closing)
1140
1141
  continue;
@@ -1143,6 +1144,11 @@ export function parseFile(source, options = {}) {
1143
1144
  actionFunctions.push(actionExpr);
1144
1145
  }
1145
1146
  for (const [attrName, attrValue] of tag.attrs.entries()) {
1147
+ if (!warnedLegacyActionAttrs && (attrName === 'data-action' || attrName === 'data-args')) {
1148
+ warnedLegacyActionAttrs = true;
1149
+ console.warn(`[kuratchi] ${options.filePath || kind}: authored data-action/data-args are deprecated. ` +
1150
+ `Use data-post={fn(...)} or action={fn} instead.`);
1151
+ }
1146
1152
  if (/^on[A-Za-z]+$/i.test(attrName)) {
1147
1153
  const actionCall = extractCallExpression(attrValue);
1148
1154
  if (actionCall && !actionFunctions.includes(actionCall.fnName))
@@ -383,7 +383,11 @@ function buildDurableObjectBlock(opts) {
383
383
  doResolverLines.push(` });`);
384
384
  }
385
385
  else {
386
- doResolverLines.push(` // No 'stubId' config for ${doEntry.binding} - stub must be obtained manually`);
386
+ doResolverLines.push(` __registerDoResolver('${doEntry.binding}', async () => {`);
387
+ doResolverLines.push(` const __ns = __env['${doEntry.binding}'];`);
388
+ doResolverLines.push(` if (!__ns?.idFromName || !__ns?.get) return null;`);
389
+ doResolverLines.push(` return __ns.get(__ns.idFromName('global'));`);
390
+ doResolverLines.push(` });`);
387
391
  }
388
392
  }
389
393
  return {
@@ -37,6 +37,36 @@ const JS_CONTROL_PATTERNS = [
37
37
  function isJsControlLine(line) {
38
38
  return JS_CONTROL_PATTERNS.some(p => p.test(line));
39
39
  }
40
+ /** HTML boolean attributes that should be present or absent, never have a value */
41
+ const BOOLEAN_ATTRIBUTES = new Set([
42
+ 'disabled',
43
+ 'checked',
44
+ 'selected',
45
+ 'readonly',
46
+ 'required',
47
+ 'hidden',
48
+ 'open',
49
+ 'autofocus',
50
+ 'autoplay',
51
+ 'controls',
52
+ 'default',
53
+ 'defer',
54
+ 'formnovalidate',
55
+ 'inert',
56
+ 'ismap',
57
+ 'itemscope',
58
+ 'loop',
59
+ 'multiple',
60
+ 'muted',
61
+ 'nomodule',
62
+ 'novalidate',
63
+ 'playsinline',
64
+ 'reversed',
65
+ 'async',
66
+ ]);
67
+ function isBooleanAttribute(name) {
68
+ return BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
69
+ }
40
70
  const FRAGMENT_OPEN_MARKER = '<!--__KURATCHI_FRAGMENT_OPEN:';
41
71
  const FRAGMENT_CLOSE_MARKER = '<!--__KURATCHI_FRAGMENT_CLOSE-->';
42
72
  export function splitTemplateRenderSections(template) {
@@ -1130,9 +1160,13 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
1130
1160
  pos = closeIdx + 1;
1131
1161
  continue;
1132
1162
  }
1133
- else if (attrName === 'disabled' || attrName === 'checked' || attrName === 'hidden' || attrName === 'readonly') {
1134
- // Boolean attributes: disabled={expr} → conditionally include
1135
- result += `"\${${inner} ? '' : undefined}"`;
1163
+ else if (isBooleanAttribute(attrName)) {
1164
+ // Boolean attributes: disabled={expr} conditionally include the attribute or omit entirely
1165
+ // Remove the trailing "attrName=" we already appended, we'll handle it with a ternary
1166
+ result = result.replace(new RegExp(`\\s*${attrName}=$`), '');
1167
+ result += `\${${inner} ? ' ${attrName}' : ''}`;
1168
+ pos = closeIdx + 1;
1169
+ continue;
1136
1170
  }
1137
1171
  else {
1138
1172
  // Regular attribute: value={expr} → value="escaped"
@@ -1,5 +1,26 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ function nextMigrationTag(existing) {
4
+ const used = new Set(existing
5
+ .map((entry) => (entry && typeof entry.tag === 'string' ? entry.tag : null))
6
+ .filter(Boolean));
7
+ let maxNumeric = 0;
8
+ for (const tag of used) {
9
+ const match = /^v(\d+)$/i.exec(String(tag));
10
+ if (match) {
11
+ maxNumeric = Math.max(maxNumeric, Number(match[1]));
12
+ }
13
+ }
14
+ let candidate = `v${maxNumeric > 0 ? maxNumeric + 1 : 1}`;
15
+ if (!used.has(candidate))
16
+ return candidate;
17
+ let index = maxNumeric + 1;
18
+ while (used.has(candidate)) {
19
+ index += 1;
20
+ candidate = `v${index}`;
21
+ }
22
+ return candidate;
23
+ }
3
24
  function stripJsonComments(content) {
4
25
  let result = '';
5
26
  let i = 0;
@@ -169,8 +190,34 @@ export function syncWranglerConfig(opts) {
169
190
  changed = true;
170
191
  console.log(`[kuratchi] Added durable_object "${durableObject.binding}" to wrangler config`);
171
192
  }
193
+ else if (existing.class_name !== durableObject.className) {
194
+ existing.class_name = durableObject.className;
195
+ changed = true;
196
+ console.log(`[kuratchi] Updated durable_object "${durableObject.binding}" class_name to "${durableObject.className}"`);
197
+ }
172
198
  }
173
199
  wranglerConfig.durable_objects.bindings = existingBindings;
200
+ const existingMigrations = Array.isArray(wranglerConfig.migrations) ? wranglerConfig.migrations : [];
201
+ const knownClasses = new Set();
202
+ for (const migration of existingMigrations) {
203
+ const newClasses = Array.isArray(migration?.new_sqlite_classes) ? migration.new_sqlite_classes : [];
204
+ for (const className of newClasses) {
205
+ if (typeof className === 'string' && className)
206
+ knownClasses.add(className);
207
+ }
208
+ }
209
+ const missingClasses = opts.config.durableObjects
210
+ .map((entry) => entry.className)
211
+ .filter((className) => !knownClasses.has(className));
212
+ if (missingClasses.length > 0) {
213
+ existingMigrations.push({
214
+ tag: nextMigrationTag(existingMigrations),
215
+ new_sqlite_classes: missingClasses,
216
+ });
217
+ wranglerConfig.migrations = existingMigrations;
218
+ changed = true;
219
+ console.log(`[kuratchi] Added durable object migration for ${missingClasses.join(', ')}`);
220
+ }
174
221
  }
175
222
  if (opts.config.assetsDirectory !== undefined) {
176
223
  const existing = wranglerConfig.assets;
package/dist/create.js CHANGED
@@ -192,11 +192,11 @@ function scaffold(dir, opts) {
192
192
  if (orm || enableDO) {
193
193
  dirs.push('src/schemas');
194
194
  }
195
- if (orm) {
196
- dirs.push('src/database');
195
+ if (orm || enableDO || auth) {
196
+ dirs.push('src/server');
197
197
  }
198
198
  if (enableDO) {
199
- dirs.push('src/server', 'src/routes/notes');
199
+ dirs.push('src/routes/notes');
200
200
  }
201
201
  if (auth) {
202
202
  dirs.push('src/routes/auth', 'src/routes/auth/login', 'src/routes/auth/signup', 'src/routes/admin');
@@ -214,19 +214,19 @@ function scaffold(dir, opts) {
214
214
  write(dir, 'src/routes/index.html', genLandingPage(opts));
215
215
  if (orm) {
216
216
  write(dir, 'src/schemas/app.ts', genSchema(opts));
217
- write(dir, 'src/database/items.ts', genItemsCrud());
217
+ write(dir, 'src/server/items.ts', genItemsCrud());
218
218
  write(dir, 'src/routes/items/index.html', genItemsPage());
219
219
  }
220
220
  if (enableDO) {
221
221
  write(dir, 'src/schemas/notes.ts', genNotesSchema());
222
222
  write(dir, 'src/server/notes.do.ts', genNotesDoHandler());
223
- write(dir, 'src/database/notes.ts', genNotesDb());
223
+ write(dir, 'src/server/notes.ts', genNotesDb());
224
224
  write(dir, 'src/routes/notes/index.html', genNotesPage());
225
225
  }
226
226
  if (auth) {
227
227
  write(dir, '.dev.vars', genDevVars());
228
- write(dir, 'src/database/auth.ts', genAuthFunctions());
229
- write(dir, 'src/database/admin.ts', genAdminLoader());
228
+ write(dir, 'src/server/auth.ts', genAuthFunctions());
229
+ write(dir, 'src/server/admin.ts', genAdminLoader());
230
230
  write(dir, 'src/routes/auth/login/index.html', genLoginPage());
231
231
  write(dir, 'src/routes/auth/signup/index.html', genSignupPage());
232
232
  write(dir, 'src/routes/admin/index.html', genAdminPage());
@@ -449,7 +449,7 @@ export async function deleteNote(id: number): Promise<void> {
449
449
  }
450
450
  function genNotesPage() {
451
451
  return `<script>
452
- import { getNotes, addNote, deleteNote } from '$database/notes';
452
+ import { getNotes, addNote, deleteNote } from '$server/notes';
453
453
 
454
454
  const notes = await getNotes();
455
455
  </script>
@@ -473,7 +473,7 @@ if (notes.length === 0) {
473
473
  for (const note of notes) {
474
474
  <article>
475
475
  <span>{note.title}</span>
476
- <button data-action="deleteNote" data-args={JSON.stringify([note.id])}>Remove</button>
476
+ <button data-post={deleteNote(note.id)} data-refresh="" type="button">Remove</button>
477
477
  </article>
478
478
  }
479
479
  </section>
@@ -631,10 +631,10 @@ ${types}
631
631
  `;
632
632
  }
633
633
  function genItemsCrud() {
634
- return `import { env } from 'cloudflare:workers';
635
- import { kuratchiORM } from '@kuratchi/orm';
636
- import { redirect } from '${FRAMEWORK_PACKAGE_NAME}';
637
- import type { Item } from './schemas/app';
634
+ return `import { env } from 'cloudflare:workers';
635
+ import { kuratchiORM } from '@kuratchi/orm';
636
+ import { redirect } from '${FRAMEWORK_PACKAGE_NAME}';
637
+ import type { Item } from '../schemas/app';
638
638
 
639
639
  const db = kuratchiORM(() => (env as any).DB);
640
640
 
@@ -664,7 +664,7 @@ export async function toggleItem(id: number): Promise<void> {
664
664
  }
665
665
  function genItemsPage() {
666
666
  return `<script>
667
- import { getItems, addItem, deleteItem, toggleItem } from '$database/items';
667
+ import { getItems, addItem, deleteItem, toggleItem } from '$server/items';
668
668
  import EmptyState from '@kuratchi/ui/empty-state.html';
669
669
 
670
670
  const items = await getItems();
@@ -690,10 +690,10 @@ if (items.length === 0) {
690
690
  <article>
691
691
  <span style={item.done ? 'text-decoration: line-through; opacity: 0.5' : ''}>{item.title}</span>
692
692
  <div>
693
- <button data-action="toggleItem" data-args={JSON.stringify([item.id])}>
693
+ <button data-post={toggleItem(item.id)} data-refresh="" type="button">
694
694
  {item.done ? '↩' : '✓'}
695
695
  </button>
696
- <button data-action="deleteItem" data-args={JSON.stringify([item.id])}>✕</button>
696
+ <button data-post={deleteItem(item.id)} data-refresh="" type="button">✕</button>
697
697
  </div>
698
698
  </article>
699
699
  }
@@ -905,7 +905,7 @@ export async function getAdminData() {
905
905
  }
906
906
  function genLoginPage() {
907
907
  return `<script>
908
- import { signIn } from '$database/auth';
908
+ import { signIn } from '$server/auth';
909
909
  import AuthCard from '@kuratchi/ui/auth-card.html';
910
910
  </script>
911
911
 
@@ -933,7 +933,7 @@ function genLoginPage() {
933
933
  }
934
934
  function genSignupPage() {
935
935
  return `<script>
936
- import { signUp } from '$database/auth';
936
+ import { signUp } from '$server/auth';
937
937
  import AuthCard from '@kuratchi/ui/auth-card.html';
938
938
  </script>
939
939
 
@@ -965,7 +965,7 @@ function genSignupPage() {
965
965
  }
966
966
  function genAdminPage() {
967
967
  return `<script>
968
- import { getAdminData, signOut } from '$database/admin';
968
+ import { getAdminData, signOut } from '$server/admin';
969
969
  import Badge from '@kuratchi/ui/badge.html';
970
970
  import Card from '@kuratchi/ui/card.html';
971
971
  import DataList from '@kuratchi/ui/data-list.html';
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { SchemaValidationError, schema, } from './runtime/schema.js';
12
12
  export { ActionError } from './runtime/action.js';
13
13
  export { PageError } from './runtime/page-error.js';
14
14
  export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
15
- export type { AppConfig, kuratchiConfig, DatabaseConfig, AuthConfig, RouteContext, RouteModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './runtime/types.js';
15
+ export type { AppConfig, kuratchiConfig, DatabaseConfig, DesktopConfig, DesktopRemoteBindingConfig, DesktopWindowConfig, AuthConfig, RouteContext, RouteModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './runtime/types.js';
16
16
  export type { RpcOf } from './runtime/do.js';
17
17
  export type { SchemaType, InferSchema } from './runtime/schema.js';
18
18
  export { url, pathname, searchParams, headers, method, params, slug } from './runtime/request.js';
@@ -0,0 +1,19 @@
1
+ export interface DesktopNotificationPayload {
2
+ title: string;
3
+ body?: string;
4
+ }
5
+ export interface DesktopCommandRequest {
6
+ command: string;
7
+ workingDirectory?: string;
8
+ timeoutMs?: number;
9
+ }
10
+ export interface DesktopCommandResult {
11
+ ok: boolean;
12
+ error?: string | null;
13
+ exitCode: number;
14
+ durationMs: number;
15
+ stdout: string;
16
+ stderr: string;
17
+ }
18
+ export declare function showDesktopNotification(payload: DesktopNotificationPayload): Promise<boolean>;
19
+ export declare function runDesktopCommand(request: DesktopCommandRequest): Promise<DesktopCommandResult | null>;