@pikku/deploy-standalone 0.12.13 → 0.12.19

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/dist/adapter.js CHANGED
@@ -1,4 +1,5 @@
1
- import { nodeBuiltinExternals, SERVER_READY_MARKER } from '@pikku/deploy';
1
+ import { assertContributorsSupported, collectContributorImports, collectContributorLines, dedupeContributors, nodeBuiltinExternals, SERVER_READY_MARKER, } from '@pikku/deploy';
2
+ export const STANDALONE_BINDING_SOURCES = ['env'];
2
3
  /**
3
4
  * Directory the built frontend is copied to, both inside the unit and beside
4
5
  * the shipped bundle. The node entry resolves it relative to itself at runtime,
@@ -24,7 +25,229 @@ const sidecarHandshakeLines = () => [
24
25
  ` watchParentProcess()`,
25
26
  ` console.log(\`${SERVER_READY_MARKER} on http://\${hostname}:\${server.port}\`)`,
26
27
  ];
27
- const SIDECAR_RUNTIME_IMPORT = `import { watchParentProcess } from '@pikku/deploy-standalone/runtime'`;
28
+ /**
29
+ * The runtime helpers the entry imports. The database ones are left out of a
30
+ * build with no database, so the bundle carries no migrator it can never run.
31
+ */
32
+ const runtimeImport = (ctx) => {
33
+ const names = ['watchParentProcess', 'parseStandaloneCommand'];
34
+ if (ctx.db)
35
+ names.push('runStandaloneCommand', 'resolveMigrationsDir');
36
+ return `import { ${names.join(', ')} } from '@pikku/deploy-standalone/runtime'`;
37
+ };
38
+ /**
39
+ * Environment variable naming the directory the SQLite file lives in.
40
+ *
41
+ * The database has to outlive a release. A deploy that swaps the artifact
42
+ * directory would take the database with it if the file sat beside the bundle,
43
+ * so the path comes from the environment and points somewhere the operator
44
+ * keeps stable across releases, rather than being derived from the bundle's own
45
+ * location the way the frontend directory is.
46
+ */
47
+ const DATA_DIR_VAR = 'PIKKU_DATA_DIR';
48
+ /**
49
+ * Full override for the database file, for when it must match a path something
50
+ * else already decided — notably `pikku db migrate`, which has to open the same
51
+ * file this opens or the app runs against an unmigrated database.
52
+ */
53
+ const DATABASE_FILE_VAR = 'PIKKU_DATABASE_FILE';
54
+ const DEFAULT_DATABASE_FILENAME = 'pikku.db';
55
+ /**
56
+ * The dialect factory each runtime opens SQLite with. bun cannot use the node
57
+ * one — `bun:sqlite` is a different driver, and the node build reaches for
58
+ * `node:sqlite`, which a compiled bun binary does not carry.
59
+ */
60
+ const SQLITE_FACTORY = {
61
+ node: {
62
+ specifier: '@pikku/kysely-node-sqlite',
63
+ fn: 'createNodeSqliteKysely',
64
+ },
65
+ bun: { specifier: '@pikku/kysely-bun-sqlite', fn: 'createBunSqliteKysely' },
66
+ };
67
+ /**
68
+ * The environment variable a Postgres build reads its connection string from.
69
+ *
70
+ * The same name every other pikku host uses, so an artifact dropped onto a
71
+ * machine that already runs a pikku app needs no new variable.
72
+ */
73
+ const DATABASE_URL_VAR = 'DATABASE_URL';
74
+ /** Imports the coercion plugin, for an app that generated a map. */
75
+ const coercionImportLines = (coercionImportPath) => [
76
+ `import { createCoercionPlugin } from '@pikku/kysely'`,
77
+ `import { coercionMap as __pikkuCoercionMap } from '${coercionImportPath}'`,
78
+ ];
79
+ /** Imports a database-backed entry needs on top of the common set. */
80
+ const dbImportLines = (runtime, db) => [
81
+ ...(db.engine === 'sqlite'
82
+ ? [
83
+ `import { ${SQLITE_FACTORY[runtime].fn} } from '${SQLITE_FACTORY[runtime].specifier}'`,
84
+ `import { mkdirSync as __pikkuMkdirSync } from 'node:fs'`,
85
+ ]
86
+ : [`import { PikkuKysely } from '@pikku/kysely-postgres'`]),
87
+ ...(db.coercionImportPath ? coercionImportLines(db.coercionImportPath) : []),
88
+ ];
89
+ /**
90
+ * Opens the database before services are built, so `createSingletonServices`
91
+ * receives `kysely` exactly as a hosted runtime would hand it over.
92
+ *
93
+ * Creating the directory rather than requiring it is deliberate: the artifact
94
+ * is expected to start on a machine where nothing has run yet, and a missing
95
+ * parent directory is the difference between a first boot that works and one
96
+ * that needs a documented mkdir nobody reads.
97
+ */
98
+ const dbSetupLines = (runtime, db) => {
99
+ const plugins = db.coercionImportPath
100
+ ? `[createCoercionPlugin({ map: __pikkuCoercionMap })]`
101
+ : `[]`;
102
+ if (db.engine === 'sqlite') {
103
+ return [
104
+ ` const __pikkuDbFile = process.env.${DATABASE_FILE_VAR}`,
105
+ ` ? process.env.${DATABASE_FILE_VAR}`,
106
+ ` : __pikkuJoin(__pikkuRequireDataDir(), '${DEFAULT_DATABASE_FILENAME}')`,
107
+ ` __pikkuMkdirSync(__pikkuDirname(__pikkuDbFile), { recursive: true })`,
108
+ ` const kysely = ${SQLITE_FACTORY[runtime].fn}({`,
109
+ ` filename: __pikkuDbFile,`,
110
+ ` plugins: ${plugins},`,
111
+ ` })`,
112
+ ];
113
+ }
114
+ return [
115
+ ` const __pikkuDbUrl = process.env.${DATABASE_URL_VAR}`,
116
+ ` if (!__pikkuDbUrl) {`,
117
+ ` throw new Error(`,
118
+ ` 'This build connects to Postgres, so it needs ${DATABASE_URL_VAR} set to the database it should open.'`,
119
+ ` )`,
120
+ ` }`,
121
+ ` const __pikkuPg = new PikkuKysely(logger, __pikkuDbUrl)`,
122
+ ` await __pikkuPg.init()`,
123
+ ...(db.coercionImportPath
124
+ ? [
125
+ ` const kysely = __pikkuPg.kysely.withPlugin(`,
126
+ ` createCoercionPlugin({ map: __pikkuCoercionMap })`,
127
+ ` )`,
128
+ ]
129
+ : [` const kysely = __pikkuPg.kysely`]),
130
+ ];
131
+ };
132
+ /**
133
+ * Where the migrations sit relative to the running artifact.
134
+ *
135
+ * A node bundle reads them from its own directory. A compiled bun binary has no
136
+ * directory — `import.meta.url` points inside the embedded filesystem — so it
137
+ * resolves them beside the executable, which is where an operator unpacking an
138
+ * artifact puts them.
139
+ */
140
+ const bundleDirExpression = (runtime) => runtime === 'node'
141
+ ? `__pikkuDirname(__pikkuFileURLToPath(import.meta.url))`
142
+ : `__pikkuDirname(process.execPath)`;
143
+ /**
144
+ * The command line, parsed before anything is opened.
145
+ *
146
+ * `version` and `help` have to answer without a database, a config factory or a
147
+ * port, because the machine asking may be one where none of the three work yet
148
+ * — which is exactly when someone runs them.
149
+ */
150
+ const commandParseLines = (ctx) => [
151
+ `const __pikkuCommand = parseStandaloneCommand(process.argv.slice(2), {`,
152
+ ` version: '${(ctx.version ?? 'unknown').replace(/'/g, "\\'")}',`,
153
+ ` hasDb: ${Boolean(ctx.db)},`,
154
+ ...(ctx.db ? [` engine: '${ctx.db.engine}',`] : []),
155
+ `})`,
156
+ `if (__pikkuCommand.kind === 'exit') process.exit(__pikkuCommand.code)`,
157
+ ];
158
+ /**
159
+ * Runs a non-serve command against the database the app itself just opened, and
160
+ * stops before a port is bound.
161
+ *
162
+ * Reusing the app's own connection is the point: a command that resolved its
163
+ * own would be free to migrate a different database than the next `serve`
164
+ * reads, and the two would only disagree once in production.
165
+ */
166
+ const commandDispatchLines = (runtime, ctx) => {
167
+ if (!ctx.db) {
168
+ return [` if (__pikkuCommand.kind !== 'serve') process.exit(0)`, ``];
169
+ }
170
+ const dir = `__pikkuJoin(${bundleDirExpression(runtime)}, 'db', '${ctx.db.engine}')`;
171
+ const handle = ctx.db.engine === 'sqlite'
172
+ ? `databaseFile: __pikkuDbFile,`
173
+ : `sql: __pikkuPg.sql,`;
174
+ return [
175
+ ` const __pikkuDbCommandTarget = {`,
176
+ ` engine: '${ctx.db.engine}',`,
177
+ ` migrationsDir: resolveMigrationsDir(${dir}),`,
178
+ ` ${handle}`,
179
+ ` }`,
180
+ ` if ((await runStandaloneCommand(__pikkuCommand, __pikkuDbCommandTarget)) === 'done') {`,
181
+ ...(ctx.db.engine === 'postgres' ? [` await __pikkuPg.close()`] : []),
182
+ ` return`,
183
+ ` }`,
184
+ ``,
185
+ ];
186
+ };
187
+ /** Imports the app's own lifecycle module, when it declares one. */
188
+ const lifecycleImportLines = (lifecycle) => [
189
+ `import { ${lifecycle.variable} as __pikkuLifecycle } from '${lifecycle.importPath}'`,
190
+ ];
191
+ /**
192
+ * Runs the app's start hooks around the port opening, the same order and the
193
+ * same services `pikku dev` gives them.
194
+ *
195
+ * `beforeStart` runs after `init` so a hook can rely on everything the server
196
+ * resolved, and before `start` so work that must finish before the first
197
+ * request — a seeded admin account, a schema probe — is finished when one
198
+ * arrives.
199
+ */
200
+ const lifecycleStartLines = () => [
201
+ ` await __pikkuLifecycle?.beforeStart?.(singletonServices)`,
202
+ ];
203
+ const lifecycleAfterStartLines = () => [
204
+ ` await __pikkuLifecycle?.afterStart?.(singletonServices)`,
205
+ ];
206
+ /**
207
+ * Hands the stop hooks to the signal handler that owns the shutdown.
208
+ *
209
+ * The connection pool is closed in `afterStop`, once the app's own hook and the
210
+ * server have both finished with it — a pool closed any earlier takes the
211
+ * queries they are still allowed to make down with it. SQLite needs no
212
+ * counterpart: the process exiting releases the file.
213
+ */
214
+ const shutdownHooksArg = (ctx) => {
215
+ const before = [];
216
+ const after = [];
217
+ if (ctx.lifecycle) {
218
+ before.push(`await __pikkuLifecycle?.beforeStop?.(singletonServices)`);
219
+ after.push(`await __pikkuLifecycle?.afterStop?.(singletonServices)`);
220
+ }
221
+ if (ctx.db?.engine === 'postgres') {
222
+ after.push(`await __pikkuPg.close()`);
223
+ }
224
+ if (before.length === 0 && after.length === 0)
225
+ return '';
226
+ const hooks = [];
227
+ if (before.length > 0) {
228
+ hooks.push(`beforeStop: async () => { ${before.join('; ')} }`);
229
+ }
230
+ if (after.length > 0) {
231
+ hooks.push(`afterStop: async () => { ${after.join('; ')} }`);
232
+ }
233
+ return `{ ${hooks.join(', ')} }`;
234
+ };
235
+ /**
236
+ * Fails with the variable's name rather than whatever SQLite says about a path
237
+ * of `undefined/pikku.db`, which is the error an operator would otherwise have
238
+ * to work backwards from.
239
+ */
240
+ const dataDirHelperLines = () => [
241
+ `function __pikkuRequireDataDir() {`,
242
+ ` const dir = process.env.${DATA_DIR_VAR}`,
243
+ ` if (!dir) {`,
244
+ ` throw new Error(`,
245
+ ` 'This build bundles a SQLite database, so it needs somewhere to keep it. Set ${DATA_DIR_VAR} to a writable directory that survives a release swap, or set ${DATABASE_FILE_VAR} to the database file itself.'`,
246
+ ` )`,
247
+ ` }`,
248
+ ` return dir`,
249
+ `}`,
250
+ ];
28
251
  /**
29
252
  * `rustc -vV`, or nothing when no toolchain is installed. The triple then falls
30
253
  * back to the Node platform pair, which is right for every ordinary host — the
@@ -40,6 +263,15 @@ const rustcHostOutput = async () => {
40
263
  return undefined;
41
264
  }
42
265
  };
266
+ const contributorPlatform = (ctx) => {
267
+ const services = ctx.unit.services ?? [];
268
+ return {
269
+ serviceNames: services.map((s) => s.sourceServiceName),
270
+ needsQueue: services.some((s) => s.capability === 'queue'),
271
+ needsWorkflow: services.some((s) => s.capability === 'workflow-state'),
272
+ needsAgent: services.some((s) => s.capability === 'ai-storage' || s.capability === 'ai-model'),
273
+ };
274
+ };
43
275
  export class StandaloneProviderAdapter {
44
276
  name = 'standalone';
45
277
  deployDirName = 'standalone';
@@ -49,12 +281,48 @@ export class StandaloneProviderAdapter {
49
281
  projectDir;
50
282
  desktopIdentifier;
51
283
  desktopUrl;
284
+ contributors;
52
285
  constructor(options = {}) {
53
286
  this.runtime = options.runtime ?? 'node';
54
287
  this.desktop = options.desktop ?? Boolean(options.desktopUrl);
55
288
  this.projectDir = options.projectDir;
56
289
  this.desktopIdentifier = options.desktopIdentifier;
57
290
  this.desktopUrl = options.desktopUrl;
291
+ this.contributors = dedupeContributors(options.contributors);
292
+ assertContributorsSupported(this.contributors, STANDALONE_BINDING_SOURCES, this.name);
293
+ }
294
+ contributorImportLines(ctx) {
295
+ if (this.contributors.length === 0)
296
+ return [];
297
+ return collectContributorImports(this.contributors, contributorPlatform(ctx));
298
+ }
299
+ platformServicesBlock(ctx) {
300
+ if (this.contributors.length === 0)
301
+ return [];
302
+ return [
303
+ `const createPlatformServices = async (env: Record<string, string | undefined>): Promise<${ctx.servicesType}> => {`,
304
+ ` const services: ${ctx.servicesType} = {}`,
305
+ ...collectContributorLines(this.contributors, {
306
+ ctx,
307
+ platform: contributorPlatform(ctx),
308
+ isGateway: false,
309
+ }),
310
+ ` return services`,
311
+ `}`,
312
+ ``,
313
+ ];
314
+ }
315
+ platformServicesCallLines() {
316
+ if (this.contributors.length === 0)
317
+ return [];
318
+ return [
319
+ ` const platformServices = await createPlatformServices(process.env as Record<string, string | undefined>)`,
320
+ ];
321
+ }
322
+ platformServicesSpreadLines() {
323
+ if (this.contributors.length === 0)
324
+ return [];
325
+ return [` ...platformServices,`];
58
326
  }
59
327
  generateEntrySource(ctx) {
60
328
  if (this.runtime === 'bun') {
@@ -73,13 +341,16 @@ export class StandaloneProviderAdapter {
73
341
  `import { PikkuNodeHTTPServer } from '@pikku/node-http-server'`,
74
342
  `import { DEFAULT_WS_MAX_PAYLOAD, pikkuWebsocketHandler } from '@pikku/ws'`,
75
343
  `import { WebSocketServer } from 'ws'`,
76
- SIDECAR_RUNTIME_IMPORT,
77
- ...(ctx.frontend
344
+ runtimeImport(ctx),
345
+ ...(ctx.frontend || ctx.db
78
346
  ? [
79
347
  `import { dirname as __pikkuDirname, join as __pikkuJoin } from 'node:path'`,
80
348
  `import { fileURLToPath as __pikkuFileURLToPath } from 'node:url'`,
81
349
  ]
82
350
  : []),
351
+ ...(ctx.db ? dbImportLines('node', ctx.db) : []),
352
+ ...(ctx.lifecycle ? lifecycleImportLines(ctx.lifecycle) : []),
353
+ ...this.contributorImportLines(ctx),
83
354
  ``,
84
355
  ctx.configImport,
85
356
  ctx.servicesImport,
@@ -91,8 +362,12 @@ export class StandaloneProviderAdapter {
91
362
  `const port = parseInt(process.env.PORT || '3000', 10)`,
92
363
  `const hostname = process.env.HOST || '0.0.0.0'`,
93
364
  ``,
365
+ ...commandParseLines(ctx),
366
+ ``,
367
+ ...this.platformServicesBlock(ctx),
94
368
  `async function main() {`,
95
369
  ` const config = await ${ctx.configVar}()`,
370
+ ...this.platformServicesCallLines(),
96
371
  ` const schedulerService = new InMemorySchedulerService()`,
97
372
  ` const queueService = new InMemoryQueueService()`,
98
373
  ` const workflowService = new InMemoryWorkflowService()`,
@@ -100,14 +375,18 @@ export class StandaloneProviderAdapter {
100
375
  ` const eventHub = new LocalEventHubService()`,
101
376
  ` workflowService.wireQueueWorkers()`,
102
377
  ` wireAgentScorerQueueWorkers()`,
378
+ ...(ctx.db ? dbSetupLines('node', ctx.db) : []),
379
+ ...commandDispatchLines('node', ctx),
103
380
  ` const singletonServices = await ${ctx.servicesVar}(config, {`,
104
381
  ` logger,`,
382
+ ...(ctx.db ? [` kysely,`] : []),
105
383
  ` schedulerService,`,
106
384
  ` queueService,`,
107
385
  ` workflowService,`,
108
386
  ` workflowRunService: workflowService,`,
109
387
  ` triggerService,`,
110
388
  ` eventHub,`,
389
+ ...this.platformServicesSpreadLines(),
111
390
  ` })`,
112
391
  ` pikkuState(null, 'package', 'singletonServices', singletonServices)`,
113
392
  ``,
@@ -136,8 +415,10 @@ export class StandaloneProviderAdapter {
136
415
  ` await server.init()`,
137
416
  ` await schedulerService.start()`,
138
417
  ` await triggerService.start()`,
139
- ` server.enableExitOnSignals()`,
418
+ ...(ctx.lifecycle ? lifecycleStartLines() : []),
419
+ ` server.enableExitOnSignals(${shutdownHooksArg(ctx)})`,
140
420
  ` await server.start()`,
421
+ ...(ctx.lifecycle ? lifecycleAfterStartLines() : []),
141
422
  ...sidecarHandshakeLines(),
142
423
  `}`,
143
424
  ``,
@@ -146,13 +427,14 @@ export class StandaloneProviderAdapter {
146
427
  ` process.exit(1)`,
147
428
  `})`,
148
429
  ``,
430
+ ...(ctx.db?.engine === 'sqlite' ? [...dataDirHelperLines(), ``] : []),
149
431
  ].join('\n');
150
432
  }
151
433
  generateBunEntrySource(ctx) {
152
434
  return [
153
435
  `// Generated standalone entry (bun runtime) — all functions in one process`,
154
436
  `import { ConsoleLogger, InMemoryQueueService, InMemoryTriggerService, InMemoryWorkflowService } from '@pikku/core/services'`,
155
- SIDECAR_RUNTIME_IMPORT,
437
+ runtimeImport(ctx),
156
438
  `import { pikkuState } from '@pikku/core/state'`,
157
439
  `import { wireAgentScorerQueueWorkers } from '@pikku/core/agent-scorer'`,
158
440
  `import { InMemorySchedulerService } from '@pikku/schedule'`,
@@ -160,6 +442,14 @@ export class StandaloneProviderAdapter {
160
442
  ...(ctx.frontend
161
443
  ? [`import { frontendAssets } from '${STANDALONE_FRONTEND_MANIFEST}'`]
162
444
  : []),
445
+ ...(ctx.db
446
+ ? [
447
+ `import { dirname as __pikkuDirname, join as __pikkuJoin } from 'node:path'`,
448
+ ]
449
+ : []),
450
+ ...(ctx.db ? dbImportLines('bun', ctx.db) : []),
451
+ ...(ctx.lifecycle ? lifecycleImportLines(ctx.lifecycle) : []),
452
+ ...this.contributorImportLines(ctx),
163
453
  ``,
164
454
  ctx.configImport,
165
455
  ctx.servicesImport,
@@ -171,8 +461,12 @@ export class StandaloneProviderAdapter {
171
461
  `const port = parseInt(process.env.PORT || '3000', 10)`,
172
462
  `const hostname = process.env.HOST || '0.0.0.0'`,
173
463
  ``,
464
+ ...commandParseLines(ctx),
465
+ ``,
466
+ ...this.platformServicesBlock(ctx),
174
467
  `async function main() {`,
175
468
  ` const config = await ${ctx.configVar}()`,
469
+ ...this.platformServicesCallLines(),
176
470
  ` const schedulerService = new InMemorySchedulerService()`,
177
471
  ` const queueService = new InMemoryQueueService()`,
178
472
  ` const workflowService = new InMemoryWorkflowService()`,
@@ -180,14 +474,18 @@ export class StandaloneProviderAdapter {
180
474
  ` const eventHub = new BunEventHubService()`,
181
475
  ` workflowService.wireQueueWorkers()`,
182
476
  ` wireAgentScorerQueueWorkers()`,
477
+ ...(ctx.db ? dbSetupLines('bun', ctx.db) : []),
478
+ ...commandDispatchLines('bun', ctx),
183
479
  ` const singletonServices = await ${ctx.servicesVar}(config, {`,
184
480
  ` logger,`,
481
+ ...(ctx.db ? [` kysely,`] : []),
185
482
  ` schedulerService,`,
186
483
  ` queueService,`,
187
484
  ` workflowService,`,
188
485
  ` workflowRunService: workflowService,`,
189
486
  ` triggerService,`,
190
487
  ` eventHub,`,
488
+ ...this.platformServicesSpreadLines(),
191
489
  ` })`,
192
490
  ` pikkuState(null, 'package', 'singletonServices', singletonServices)`,
193
491
  ``,
@@ -208,8 +506,10 @@ export class StandaloneProviderAdapter {
208
506
  ` await server.init()`,
209
507
  ` await schedulerService.start()`,
210
508
  ` await triggerService.start()`,
211
- ` server.enableExitOnSignals()`,
509
+ ...(ctx.lifecycle ? lifecycleStartLines() : []),
510
+ ` server.enableExitOnSignals(${shutdownHooksArg(ctx)})`,
212
511
  ` await server.start()`,
512
+ ...(ctx.lifecycle ? lifecycleAfterStartLines() : []),
213
513
  ...sidecarHandshakeLines(),
214
514
  `}`,
215
515
  ``,
@@ -218,6 +518,7 @@ export class StandaloneProviderAdapter {
218
518
  ` process.exit(1)`,
219
519
  `})`,
220
520
  ``,
521
+ ...(ctx.db?.engine === 'sqlite' ? [...dataDirHelperLines(), ``] : []),
221
522
  ].join('\n');
222
523
  }
223
524
  generateUnitConfigs() {
@@ -239,6 +540,30 @@ export class StandaloneProviderAdapter {
239
540
  }
240
541
  return externals;
241
542
  }
543
+ /**
544
+ * The SQLite driver this runtime cannot load.
545
+ *
546
+ * `loadSqliteRuntime` picks its driver by looking for `globalThis.Bun`, so a
547
+ * node process never runs the bun branch — but esbuild still follows the
548
+ * import, and `bun:sqlite` sits at the top of that module as a static import
549
+ * it cannot resolve. Left in, the bundle fails to build; marked external, it
550
+ * becomes a top-level import node fails to load. Stubbing removes the branch
551
+ * that was already dead.
552
+ */
553
+ getStubModules() {
554
+ if (this.runtime === 'bun')
555
+ return [];
556
+ return ['sqlite-runtime-bun'];
557
+ }
558
+ /**
559
+ * The bun bundle is not the artifact that runs — `bun build --compile` turns
560
+ * it into the binary — so esbuild must not rename anything bun will rename
561
+ * again. See `getMangleIdentifiers` on the adapter interface for the boot
562
+ * failure the two passes produce together.
563
+ */
564
+ getMangleIdentifiers() {
565
+ return this.runtime !== 'bun';
566
+ }
242
567
  getPlatform() {
243
568
  return 'node';
244
569
  }
package/dist/index.d.ts CHANGED
@@ -12,4 +12,5 @@
12
12
  import { StandaloneProviderAdapter, type StandaloneProviderAdapterOptions } from './adapter.js';
13
13
  export { StandaloneProviderAdapter };
14
14
  export type { StandaloneProviderAdapterOptions } from './adapter.js';
15
+ export type { PlatformServiceContributor } from '@pikku/deploy';
15
16
  export declare const createAdapter: (options?: StandaloneProviderAdapterOptions) => StandaloneProviderAdapter;
@@ -0,0 +1,75 @@
1
+ /** Where the migrations live, when the operator has moved them. */
2
+ export declare const MIGRATIONS_DIR_ENV = "PIKKU_MIGRATIONS_DIR";
3
+ export interface StandaloneSqliteDb {
4
+ engine: 'sqlite';
5
+ migrationsDir: string;
6
+ /** The file the app itself opens, so a migration cannot target another one. */
7
+ databaseFile: string;
8
+ }
9
+ /**
10
+ * The postgres.js tagged template the app is already connected through.
11
+ *
12
+ * Taken rather than constructed so a migration runs on the app's own pool: a
13
+ * second connection would need the credentials resolved twice and could reach a
14
+ * different database than the one the next `serve` opens.
15
+ */
16
+ export interface PostgresSql {
17
+ unsafe(query: string, parameters?: unknown[]): Promise<any> & {
18
+ simple(): Promise<any>;
19
+ };
20
+ begin<T>(handler: (sql: PostgresSql) => Promise<T>): Promise<T>;
21
+ }
22
+ export interface StandalonePostgresDb {
23
+ engine: 'postgres';
24
+ migrationsDir: string;
25
+ sql: PostgresSql;
26
+ }
27
+ export type StandaloneDb = StandaloneSqliteDb | StandalonePostgresDb;
28
+ export type StandaloneCommand = {
29
+ kind: 'serve';
30
+ } | {
31
+ kind: 'db';
32
+ action: 'migrate' | 'status';
33
+ } | {
34
+ kind: 'backup';
35
+ destination: string;
36
+ } | {
37
+ kind: 'exit';
38
+ code: number;
39
+ };
40
+ export interface ParseOptions {
41
+ version: string;
42
+ /** False for a build with no database, whose db commands cannot be answered. */
43
+ hasDb: boolean;
44
+ engine?: 'sqlite' | 'postgres';
45
+ write?: (line: string) => void;
46
+ }
47
+ export declare function parseStandaloneCommand(argv: string[], options: ParseOptions): StandaloneCommand;
48
+ /**
49
+ * The migrations directory, honouring an operator who keeps them elsewhere.
50
+ *
51
+ * `bundleDir` is where the build put them, which is the same `db/<engine>/`
52
+ * path Fabric's build container stages into an artifact — so an artifact from
53
+ * either producer answers `db migrate` without being told where to look.
54
+ */
55
+ export declare const resolveMigrationsDir: (bundleDir: string, env?: Record<string, string | undefined>) => string;
56
+ export interface CommandOutput {
57
+ write(line: string): void;
58
+ }
59
+ export declare function runDbCommand(action: 'migrate' | 'status', db: StandaloneDb, out?: CommandOutput): Promise<void>;
60
+ /**
61
+ * Copy the SQLite database somewhere else, while the app may be running.
62
+ *
63
+ * `VACUUM INTO` rather than copying the file: a plain copy taken while another
64
+ * process is mid-write captures a torn page and a write-ahead log it has no
65
+ * copy of, which restores as a corrupt database and only says so later.
66
+ */
67
+ export declare function runBackupCommand(destination: string, db: StandaloneSqliteDb, out?: CommandOutput): Promise<void>;
68
+ /**
69
+ * Run whatever the argv asked for, and say whether the caller should serve.
70
+ *
71
+ * The database is passed already open, because the entry has to open it the one
72
+ * way the app does — a command that resolved its own connection could migrate a
73
+ * different database than the next `serve` reads.
74
+ */
75
+ export declare function runStandaloneCommand(command: StandaloneCommand, db: StandaloneDb | undefined, out?: CommandOutput): Promise<'serve' | 'done'>;