@kky42/pievo-server 2.4.14 → 4.1.0

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 (95) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/MarkdownView-BuFKr9Jg.js +66 -0
  3. package/.output/public/assets/{actionUi-5PtES4q0.js → actionUi-BZrLpgHT.js} +1 -1
  4. package/.output/public/assets/app-x1CeeY7N.css +2 -0
  5. package/.output/public/assets/index-D0FIKiiW.js +10 -0
  6. package/.output/public/assets/loops._loopId-DTkIz4C0.js +2 -0
  7. package/.output/public/assets/loops._loopId_.runs._runId-CsV2Ih4Z.js +3 -0
  8. package/.output/public/assets/preload-helper-BSxGu4d0.js +3 -0
  9. package/.output/public/assets/{routes-Dm_3J2aV.js → routes-BIOJZsig.js} +1 -1
  10. package/.output/public/assets/routes-m4Z90DTR.js +1 -0
  11. package/.output/server/_chunks/ssr-renderer.mjs +1 -1
  12. package/.output/server/_libs/{drizzle-orm+postgres.mjs → drizzle-orm.mjs} +93 -2218
  13. package/.output/server/_ssr/{MachinesModal-BaEtzycr.mjs → MachinesModal-CLBZxZkq.mjs} +47 -47
  14. package/.output/server/_ssr/MarkdownView-D1J7ImYF.mjs +70 -0
  15. package/.output/server/_ssr/{actionUi-5HOCZnom.mjs → actionUi-DE-Pb2tv.mjs} +1 -2
  16. package/.output/server/_ssr/artifactFiles-B0NPRDOR.mjs +52 -0
  17. package/.output/server/_ssr/{artifactKind-DwDr8IYc.mjs → artifactKind-a2oxT2wQ.mjs} +4 -10
  18. package/.output/server/_ssr/{boot-C472kLwY.mjs → boot-DmpKtb0A.mjs} +2659 -890
  19. package/.output/server/_ssr/lifecycleUi-DEjM8Etm.mjs +106 -0
  20. package/.output/server/_ssr/{loopApi-DT751TJA.mjs → loopApi-DK1lQPrx.mjs} +53 -69
  21. package/.output/server/_ssr/{loopProjection-qaD2I48a.mjs → loopProjection-DdFivf7-.mjs} +6 -24
  22. package/.output/server/_ssr/{loops._loopId-CXgw2Rz1.mjs → loops._loopId-DVCvGSEc.mjs} +36 -116
  23. package/.output/server/_ssr/{loops._loopId_.runs._runId-SASSqOfy.mjs → loops._loopId_.runs._runId-BXbTfLlC.mjs} +35 -39
  24. package/.output/server/_ssr/{loops._loopId_.runs._runId-Dk94Z0Jq.mjs → loops._loopId_.runs._runId-Dr6E-FA5.mjs} +3 -8
  25. package/.output/server/_ssr/{machineFns-BSrIgn3f.mjs → machineFns-DLRHgDdS.mjs} +13 -50
  26. package/.output/server/_ssr/networkAccess-CIlK3ION.mjs +30 -0
  27. package/.output/server/_ssr/router-BhHsTGqu.mjs +355 -0
  28. package/.output/server/_ssr/routes-CCallyCT.mjs +12 -0
  29. package/.output/server/_ssr/{routes-Cr-9yDEH.mjs → routes-CL2Kef-Q.mjs} +91 -50
  30. package/.output/server/_ssr/{routes-DRBS4Zi4.mjs → routes-ngpX7h8D.mjs} +2 -4
  31. package/.output/server/_ssr/ssr.mjs +25 -34
  32. package/.output/server/_ssr/start-B5_9Z5W-.mjs +35 -0
  33. package/.output/server/{_tanstack-start-manifest_v-DxLIQ0cc.mjs → _tanstack-start-manifest_v-BPiJ4yWT.mjs} +9 -15
  34. package/.output/server/drizzle/0000_baseline.sql +6 -97
  35. package/.output/server/drizzle/0001_loop_files.sql +9 -0
  36. package/.output/server/drizzle/meta/_journal.json +3 -17
  37. package/.output/server/index.mjs +50 -50
  38. package/drizzle/0000_baseline.sql +6 -97
  39. package/drizzle/0001_loop_files.sql +9 -0
  40. package/drizzle/meta/_journal.json +3 -17
  41. package/package.json +4 -8
  42. package/scripts/pievo-server.mjs +49 -47
  43. package/scripts/server-cli-lib.mjs +21 -12
  44. package/scripts/server-network.mjs +139 -0
  45. package/.output/public/assets/SignIn-CIFwnZtF.js +0 -1
  46. package/.output/public/assets/app-DvHMhs7S.css +0 -2
  47. package/.output/public/assets/index-CRX2pu0W.js +0 -10
  48. package/.output/public/assets/loops._loopId-BsuV-Rmf.js +0 -67
  49. package/.output/public/assets/loops._loopId_.runs._runId-DpK_fzmZ.js +0 -3
  50. package/.output/public/assets/preload-helper-sAZOZb3G.js +0 -5
  51. package/.output/public/assets/routes-DxF_s3cD.js +0 -1
  52. package/.output/server/_libs/@aws-crypto/crc32+[...].mjs +0 -419
  53. package/.output/server/_libs/@aws-sdk/checksums+[...].mjs +0 -10732
  54. package/.output/server/_libs/@aws-sdk/client-s3+[...].mjs +0 -9475
  55. package/.output/server/_libs/@aws-sdk/credential-provider-http+[...].mjs +0 -137
  56. package/.output/server/_libs/@aws-sdk/credential-provider-ini+[...].mjs +0 -440
  57. package/.output/server/_libs/@aws-sdk/credential-provider-process+[...].mjs +0 -62
  58. package/.output/server/_libs/@aws-sdk/credential-provider-sso+[...].mjs +0 -818
  59. package/.output/server/_libs/@aws-sdk/credential-provider-web-identity+[...].mjs +0 -59
  60. package/.output/server/_libs/@smithy/credential-provider-imds+[...].mjs +0 -342
  61. package/.output/server/_libs/aws-crypto__crc32c.mjs +0 -314
  62. package/.output/server/_libs/aws-sdk__nested-clients.mjs +0 -2671
  63. package/.output/server/_ssr/SignIn-sHQaBQJC.mjs +0 -53
  64. package/.output/server/_ssr/artifactFiles-DO43wwXA.mjs +0 -60
  65. package/.output/server/_ssr/artifacts-Cd7Qx3Qp.mjs +0 -2
  66. package/.output/server/_ssr/auth-0AamrwHo.mjs +0 -38355
  67. package/.output/server/_ssr/auth-client-lSMb_Idt.mjs +0 -1070
  68. package/.output/server/_ssr/bun-sqlite-dialect-BW9W1_Ps-DLIXocA0.mjs +0 -162
  69. package/.output/server/_ssr/core-RrmWoKne.mjs +0 -5
  70. package/.output/server/_ssr/d1-sqlite-dialect-BLC8LXE6-BoFMIPr_.mjs +0 -112
  71. package/.output/server/_ssr/db-BzvMQ2kx.mjs +0 -2
  72. package/.output/server/_ssr/dist-ClzqkwxD.mjs +0 -342
  73. package/.output/server/_ssr/factory-ixUjZqJn.mjs +0 -6903
  74. package/.output/server/_ssr/kysely-adapter-DkZbmKgV.mjs +0 -2
  75. package/.output/server/_ssr/kysely-migration-tables-JkVUjPF_-bPAX9LR_.mjs +0 -19
  76. package/.output/server/_ssr/logger-DDjsYB2X.mjs +0 -2
  77. package/.output/server/_ssr/loopTags-CXx5UdGf.mjs +0 -3356
  78. package/.output/server/_ssr/node-sqlite-dialect-BbUqnnYv.mjs +0 -162
  79. package/.output/server/_ssr/router-BlLlhfJe.mjs +0 -543
  80. package/.output/server/_ssr/routes-DTBltabU.mjs +0 -27
  81. package/.output/server/_ssr/start-4ifArtKx.mjs +0 -4
  82. package/.output/server/_ssr/store-vDZ9INSa.mjs +0 -2
  83. package/.output/server/_ssr/tokens-BHiCElYD.mjs +0 -2
  84. package/.output/server/drizzle/0001_add_pi_agent.sql +0 -9
  85. package/.output/server/drizzle/0002_remove_teams.sql +0 -15
  86. package/.output/server/drizzle/0003_add_loop_tags.sql +0 -5
  87. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  88. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  89. package/.output/server/node_modules/tslib/package.json +0 -47
  90. package/.output/server/node_modules/tslib/tslib.js +0 -484
  91. package/.output/server/package.json +0 -9
  92. package/drizzle/0001_add_pi_agent.sql +0 -9
  93. package/drizzle/0002_remove_teams.sql +0 -15
  94. package/drizzle/0003_add_loop_tags.sql +0 -5
  95. package/scripts/prestart.mjs +0 -66
@@ -18,17 +18,14 @@ CREATE TABLE "blobs" (
18
18
  --> statement-breakpoint
19
19
  CREATE TABLE "connect_keys" (
20
20
  "machine_id" text PRIMARY KEY NOT NULL,
21
- "user_id" text NOT NULL,
22
- "team_id" text NOT NULL,
23
21
  "minted_at" text NOT NULL
24
22
  );
25
23
  --> statement-breakpoint
26
24
  CREATE TABLE "loops" (
27
25
  "id" text PRIMARY KEY NOT NULL,
28
- "user_id" text NOT NULL,
29
- "team_id" text NOT NULL,
30
26
  "machine_id" text NOT NULL,
31
27
  "name" text NOT NULL,
28
+ "tags" text[] DEFAULT ARRAY[]::text[] NOT NULL,
32
29
  "prompt" text NOT NULL,
33
30
  "status_keep" text NOT NULL,
34
31
  "status_no_change" text NOT NULL,
@@ -51,15 +48,15 @@ CREATE TABLE "loops" (
51
48
  "last_run_index" integer DEFAULT 0 NOT NULL,
52
49
  "created_at" text NOT NULL,
53
50
  "updated_at" text NOT NULL,
54
- CONSTRAINT "loops_agent_check" CHECK ("loops"."agent" IN ('claude-code', 'codex')),
51
+ CONSTRAINT "loops_agent_check" CHECK ("loops"."agent" IN ('claude-code', 'codex', 'pi')),
52
+ CONSTRAINT "loops_tags_count_check" CHECK (cardinality("loops"."tags") <= 4),
53
+ CONSTRAINT "loops_tags_reserved_check" CHECK (NOT ("loops"."tags" && ARRAY['all loops', 'active', 'paused', 'blocked']::text[])),
55
54
  CONSTRAINT "loops_schedule_mode_check" CHECK ("loops"."schedule_mode" IN ('cron', 'continuous')),
56
55
  CONSTRAINT "loops_cron_overlap_check" CHECK ("loops"."cron_overlap" IN ('skip', 'queue-one'))
57
56
  );
58
57
  --> statement-breakpoint
59
58
  CREATE TABLE "machines" (
60
59
  "id" text PRIMARY KEY NOT NULL,
61
- "user_id" text NOT NULL,
62
- "team_id" text NOT NULL,
63
60
  "name" text NOT NULL,
64
61
  "hostname" text,
65
62
  "platform" text,
@@ -125,40 +122,12 @@ CREATE TABLE "runs" (
125
122
  "cancel_requested_at" timestamp with time zone,
126
123
  "heartbeat_at" text,
127
124
  "report_incident" jsonb,
128
- CONSTRAINT "runs_agent_check" CHECK ("runs"."agent" IS NULL OR "runs"."agent" IN ('claude-code', 'codex')),
125
+ CONSTRAINT "runs_agent_check" CHECK ("runs"."agent" IS NULL OR "runs"."agent" IN ('claude-code', 'codex', 'pi')),
129
126
  CONSTRAINT "runs_phase_check" CHECK ("runs"."phase" IN ('pending', 'running', 'done', 'error', 'canceled')),
130
127
  CONSTRAINT "runs_requested_by_check" CHECK ("runs"."requested_by" IN ('owner', 'system')),
131
128
  CONSTRAINT "runs_status_check" CHECK ("runs"."status" IS NULL OR "runs"."status" IN ('keep', 'no-change', 'block'))
132
129
  );
133
130
  --> statement-breakpoint
134
- CREATE TABLE "team_invites" (
135
- "token" text PRIMARY KEY NOT NULL,
136
- "team_id" text NOT NULL,
137
- "role" text DEFAULT 'member' NOT NULL,
138
- "invited_by_user_id" text NOT NULL,
139
- "expires_at" text NOT NULL,
140
- "redeemed_at" text,
141
- "redeemed_by_user_id" text,
142
- "created_at" text NOT NULL,
143
- CONSTRAINT "team_invites_role_check" CHECK ("team_invites"."role" IN ('owner', 'member'))
144
- );
145
- --> statement-breakpoint
146
- CREATE TABLE "team_members" (
147
- "id" text PRIMARY KEY NOT NULL,
148
- "team_id" text NOT NULL,
149
- "user_id" text NOT NULL,
150
- "role" text DEFAULT 'member' NOT NULL,
151
- "created_at" text NOT NULL,
152
- CONSTRAINT "team_members_role_check" CHECK ("team_members"."role" IN ('owner', 'member'))
153
- );
154
- --> statement-breakpoint
155
- CREATE TABLE "teams" (
156
- "id" text PRIMARY KEY NOT NULL,
157
- "name" text NOT NULL,
158
- "owner_user_id" text,
159
- "created_at" text NOT NULL
160
- );
161
- --> statement-breakpoint
162
131
  CREATE TABLE "terminal_report_incidents" (
163
132
  "id" text PRIMARY KEY NOT NULL,
164
133
  "run_id" text NOT NULL,
@@ -170,65 +139,11 @@ CREATE TABLE "terminal_report_incidents" (
170
139
  CONSTRAINT "terminal_report_incidents_disposition_check" CHECK ("terminal_report_incidents"."disposition" IN ('run-error', 'telemetry-rejected'))
171
140
  );
172
141
  --> statement-breakpoint
173
- CREATE TABLE "account" (
174
- "id" text PRIMARY KEY NOT NULL,
175
- "account_id" text NOT NULL,
176
- "provider_id" text NOT NULL,
177
- "user_id" text NOT NULL,
178
- "access_token" text,
179
- "refresh_token" text,
180
- "id_token" text,
181
- "access_token_expires_at" timestamp,
182
- "refresh_token_expires_at" timestamp,
183
- "scope" text,
184
- "password" text,
185
- "created_at" timestamp DEFAULT now() NOT NULL,
186
- "updated_at" timestamp NOT NULL
187
- );
188
- --> statement-breakpoint
189
- CREATE TABLE "session" (
190
- "id" text PRIMARY KEY NOT NULL,
191
- "expires_at" timestamp NOT NULL,
192
- "token" text NOT NULL,
193
- "created_at" timestamp DEFAULT now() NOT NULL,
194
- "updated_at" timestamp NOT NULL,
195
- "ip_address" text,
196
- "user_agent" text,
197
- "user_id" text NOT NULL,
198
- CONSTRAINT "session_token_unique" UNIQUE("token")
199
- );
200
- --> statement-breakpoint
201
- CREATE TABLE "user" (
202
- "id" text PRIMARY KEY NOT NULL,
203
- "name" text NOT NULL,
204
- "email" text NOT NULL,
205
- "email_verified" boolean DEFAULT false NOT NULL,
206
- "image" text,
207
- "created_at" timestamp DEFAULT now() NOT NULL,
208
- "updated_at" timestamp DEFAULT now() NOT NULL,
209
- CONSTRAINT "user_email_unique" UNIQUE("email")
210
- );
211
- --> statement-breakpoint
212
- CREATE TABLE "verification" (
213
- "id" text PRIMARY KEY NOT NULL,
214
- "identifier" text NOT NULL,
215
- "value" text NOT NULL,
216
- "expires_at" timestamp NOT NULL,
217
- "created_at" timestamp DEFAULT now() NOT NULL,
218
- "updated_at" timestamp DEFAULT now() NOT NULL
219
- );
220
- --> statement-breakpoint
221
- ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
222
- ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
223
142
  CREATE INDEX "artifact_files_loop_idx" ON "artifact_files" USING btree ("loop_id");--> statement-breakpoint
224
143
  CREATE UNIQUE INDEX "artifact_files_loop_path_idx" ON "artifact_files" USING btree ("loop_id","path");--> statement-breakpoint
225
144
  CREATE INDEX "artifact_files_hash_idx" ON "artifact_files" USING btree ("hash");--> statement-breakpoint
226
- CREATE INDEX "loops_user_idx" ON "loops" USING btree ("user_id");--> statement-breakpoint
227
- CREATE INDEX "loops_team_idx" ON "loops" USING btree ("team_id");--> statement-breakpoint
228
145
  CREATE INDEX "loops_machine_idx" ON "loops" USING btree ("machine_id");--> statement-breakpoint
229
146
  CREATE INDEX "delete_requested_loops" ON "loops" USING btree ("delete_requested_at") WHERE "loops"."delete_requested_at" IS NOT NULL;--> statement-breakpoint
230
- CREATE INDEX "machines_user_idx" ON "machines" USING btree ("user_id");--> statement-breakpoint
231
- CREATE INDEX "machines_team_idx" ON "machines" USING btree ("team_id");--> statement-breakpoint
232
147
  CREATE INDEX "run_leases_run_idx" ON "run_leases" USING btree ("run_id");--> statement-breakpoint
233
148
  CREATE INDEX "run_leases_loop_idx" ON "run_leases" USING btree ("loop_id");--> statement-breakpoint
234
149
  CREATE INDEX "report_receipts_created" ON "run_report_receipts" USING btree ("created_at");--> statement-breakpoint
@@ -241,11 +156,5 @@ CREATE UNIQUE INDEX "runs_loop_run_index_idx" ON "runs" USING btree ("loop_id","
241
156
  CREATE INDEX "runs_loop_terminal_history_idx" ON "runs" USING btree ("loop_id","run_index") WHERE "runs"."run_index" IS NOT NULL AND "runs"."phase" IN ('done', 'error', 'canceled');--> statement-breakpoint
242
157
  CREATE UNIQUE INDEX "runs_loop_pending_idx" ON "runs" USING btree ("loop_id") WHERE "runs"."phase" = 'pending';--> statement-breakpoint
243
158
  CREATE UNIQUE INDEX "one_running_run_per_loop" ON "runs" USING btree ("loop_id") WHERE "runs"."phase" = 'running';--> statement-breakpoint
244
- CREATE INDEX "team_invites_team_idx" ON "team_invites" USING btree ("team_id");--> statement-breakpoint
245
- CREATE INDEX "team_members_team_idx" ON "team_members" USING btree ("team_id");--> statement-breakpoint
246
- CREATE INDEX "team_members_user_idx" ON "team_members" USING btree ("user_id");--> statement-breakpoint
247
159
  CREATE INDEX "terminal_report_incidents_report_id" ON "terminal_report_incidents" USING btree ("report_id");--> statement-breakpoint
248
- CREATE INDEX "terminal_report_incidents_created" ON "terminal_report_incidents" USING btree ("created_at");--> statement-breakpoint
249
- CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
250
- CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint
251
- CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier");
160
+ CREATE INDEX "terminal_report_incidents_created" ON "terminal_report_incidents" USING btree ("created_at");
@@ -0,0 +1,9 @@
1
+ -- Loop files replace workdir-relative artifact sync (protocol v5).
2
+ -- DATA LOSS (intentional): previously synced workdir artifacts, their per-run
3
+ -- snapshots, and every loop's configured artifact paths are dropped. Old paths
4
+ -- are not valid loop file names; orphaned blob bytes are reclaimed by the next
5
+ -- storage-maintenance pass. Run history and loop configuration are otherwise kept.
6
+ DELETE FROM "artifact_files";--> statement-breakpoint
7
+ DELETE FROM "run_snapshots";--> statement-breakpoint
8
+ UPDATE "loops" SET "artifacts" = '[]'::jsonb;--> statement-breakpoint
9
+ ALTER TABLE "runs" ADD COLUMN "file_notices" jsonb;
@@ -5,29 +5,15 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "7",
8
- "when": 1785146849042,
8
+ "when": 1792651729223,
9
9
  "tag": "0000_baseline",
10
10
  "breakpoints": true
11
11
  },
12
12
  {
13
13
  "idx": 1,
14
14
  "version": "7",
15
- "when": 1785146849043,
16
- "tag": "0001_add_pi_agent",
17
- "breakpoints": true
18
- },
19
- {
20
- "idx": 2,
21
- "version": "7",
22
- "when": 1785146849044,
23
- "tag": "0002_remove_teams",
24
- "breakpoints": true
25
- },
26
- {
27
- "idx": 3,
28
- "version": "7",
29
- "when": 1786301181156,
30
- "tag": "0003_add_loop_tags",
15
+ "when": 1792900000000,
16
+ "tag": "0001_loop_files",
31
17
  "breakpoints": true
32
18
  }
33
19
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kky42/pievo-server",
3
- "version": "2.4.14",
3
+ "version": "4.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  "directory": "packages/server"
9
9
  },
10
10
  "type": "module",
11
- "description": "Pievo server — a self-hostable zero-exec control plane for scheduled agent loops.",
11
+ "description": "Pievo server — the single-user dashboard and scheduler for Pievo loops. Usually run through `pievo server`.",
12
12
  "bin": {
13
13
  "pievo-server": "scripts/pievo-server.mjs"
14
14
  },
@@ -17,7 +17,7 @@
17
17
  "drizzle",
18
18
  "scripts/pievo-server.mjs",
19
19
  "scripts/server-cli-lib.mjs",
20
- "scripts/prestart.mjs",
20
+ "scripts/server-network.mjs",
21
21
  "LICENSE"
22
22
  ],
23
23
  "publishConfig": {
@@ -30,28 +30,24 @@
30
30
  "dev": "vite dev",
31
31
  "build": "vite build && node scripts/copy-pglite-assets.mjs",
32
32
  "build:publish": "vite build && node scripts/copy-pglite-assets.mjs --strict",
33
- "start": "node scripts/prestart.mjs && node .output/server/index.mjs",
33
+ "start": "node scripts/pievo-server.mjs start --foreground",
34
34
  "routes:generate": "tsr generate",
35
35
  "typecheck": "tsr generate && tsc --noEmit",
36
36
  "test": "node --test ../../scripts/dev-profile.test.mjs && vitest run",
37
37
  "db:generate": "drizzle-kit generate",
38
- "db:migrate": "drizzle-kit migrate",
39
38
  "verify:package": "node scripts/verify-package.mjs",
40
39
  "prepublishOnly": "pnpm build:publish"
41
40
  },
42
41
  "dependencies": {
43
- "@aws-sdk/client-s3": "^3.1075.0",
44
42
  "@base-ui/react": "^1.6.0",
45
43
  "@electric-sql/pglite": "^0.5.4",
46
44
  "@tanstack/react-router": "^1.170.16",
47
45
  "@tanstack/react-start": "^1.168.26",
48
- "better-auth": "^1.6.19",
49
46
  "croner": "^10.0.1",
50
47
  "diff": "^9.0.0",
51
48
  "dompurify": "^3.4.11",
52
49
  "drizzle-orm": "^0.45.2",
53
50
  "marked": "^18.0.5",
54
- "postgres": "^3.4.9",
55
51
  "pino": "^10.3.1",
56
52
  "pino-pretty": "^13.1.3",
57
53
  "react": "^19.2.7",
@@ -24,6 +24,7 @@ import {
24
24
  withLaunchNonce,
25
25
  writePidRecord,
26
26
  } from "./server-cli-lib.mjs";
27
+ import { currentHttpsUrl, disableHttps, enableHttps, reachableUrls, tailscaleInfo } from "./server-network.mjs";
27
28
 
28
29
  const scriptFile = fileURLToPath(import.meta.url);
29
30
  const packageRoot = path.resolve(path.dirname(scriptFile), "..");
@@ -37,7 +38,6 @@ const LOCK_HANDOFF_TIMEOUT_MS = 5_000;
37
38
  function installShutdown(plan, expected) {
38
39
  let stopping = false;
39
40
  let stopSignal;
40
- let prestartChild;
41
41
  let nitroLoaded = false;
42
42
  let replayedForNitro = false;
43
43
 
@@ -55,9 +55,6 @@ function installShutdown(plan, expected) {
55
55
  stopSignal = signal;
56
56
  console.log(`[pievo-server] received ${signal}; shutting down`);
57
57
  shutdownApplication();
58
- if (prestartChild?.exitCode === null && prestartChild?.signalCode === null) {
59
- try { prestartChild.kill(signal); } catch {}
60
- }
61
58
  }
62
59
  };
63
60
 
@@ -69,8 +66,6 @@ function installShutdown(plan, expected) {
69
66
 
70
67
  return {
71
68
  get stopping() { return stopping; },
72
- trackPrestart(child) { prestartChild = child; },
73
- clearPrestart(child) { if (prestartChild === child) prestartChild = undefined; },
74
69
  nitroLoaded() {
75
70
  nitroLoaded = true;
76
71
  // If the first signal arrived while Nitro was importing, its graceful HTTP
@@ -94,25 +89,32 @@ function installShutdown(plan, expected) {
94
89
  };
95
90
  }
96
91
 
97
- async function runPrestart(plan, shutdown) {
98
- const child = spawn(process.execPath, [path.join(packageRoot, "scripts", "prestart.mjs")], {
99
- cwd: packageRoot,
100
- env: plan.env,
101
- stdio: "inherit",
102
- });
103
- shutdown.trackPrestart(child);
104
- try {
105
- const result = await new Promise((resolve, reject) => {
106
- child.once("error", reject);
107
- child.once("exit", (code, signal) => resolve({ code, signal }));
108
- });
109
- if (shutdown.stopping) throw new Error("startup interrupted by shutdown signal");
110
- if (result.code !== 0) {
111
- throw new Error(`migration prestart failed (${result.signal ? `signal ${result.signal}` : `exit ${result.code ?? "unknown"}`})`);
112
- }
113
- } finally {
114
- shutdown.clearPrestart(child);
92
+ /** Publish Tailscale HTTPS (when enabled) and pass the best connect URL to the server. */
93
+ function publishNetwork(plan) {
94
+ const tailscale = tailscaleInfo();
95
+ let https;
96
+ let httpsNote;
97
+ if (plan.https && tailscale) {
98
+ const result = enableHttps(tailscale, plan.port);
99
+ if (result.ok) https = result.url;
100
+ else httpsNote = result.reason;
115
101
  }
102
+ const urls = reachableUrls({ host: plan.host, port: plan.port, https, tailscale });
103
+ return { urls, httpsNote, plan: { ...plan, env: { ...plan.env, PIEVO_PUBLIC_URL: urls.connect } } };
104
+ }
105
+
106
+ function retractHttps(record) {
107
+ if (record?.https === false) return;
108
+ const tailscale = tailscaleInfo();
109
+ if (tailscale) disableHttps(tailscale, record.port);
110
+ }
111
+
112
+ function printUrls(urls, httpsNote) {
113
+ console.log(` local ${urls.local}`);
114
+ if (urls.lan.length) console.log(` lan ${urls.lan.join(" ")}`);
115
+ if (urls.tailscale.length) console.log(` tailscale ${urls.tailscale.join(" ")}`);
116
+ if (urls.https) console.log(` https ${urls.https}`);
117
+ if (httpsNote) console.log(` https unavailable: ${httpsNote}`);
116
118
  }
117
119
 
118
120
  async function waitForPidAuthority(plan, expected, shutdown) {
@@ -127,7 +129,7 @@ async function waitForPidAuthority(plan, expected, shutdown) {
127
129
 
128
130
  async function serve(plan, expected) {
129
131
  // Signal handling comes first: startup remains controllable while waiting for
130
- // the parent record and while the async migration child is running.
132
+ // the parent record.
131
133
  const shutdown = installShutdown(plan, expected);
132
134
  if (!(await waitForPidAuthority(plan, expected, shutdown))) {
133
135
  throw new Error("launcher did not establish pid authority before startup");
@@ -139,15 +141,6 @@ async function serve(plan, expected) {
139
141
  throw new Error("pid authority changed before startup");
140
142
  }
141
143
 
142
- await runPrestart(plan, shutdown);
143
- if (shutdown.stopping) return;
144
- const afterPrestart = readPidRecord(plan.pidFile);
145
- if (afterPrestart?.pid !== expected.pid
146
- || afterPrestart.startTime !== expected.startTime
147
- || afterPrestart.launchNonce !== expected.launchNonce) {
148
- throw new Error("pid authority changed during migration prestart");
149
- }
150
-
151
144
  try {
152
145
  await import(pathToFileURL(path.join(packageRoot, ".output", "server", "index.mjs")).href);
153
146
  shutdown.nitroLoaded();
@@ -160,7 +153,7 @@ async function serve(plan, expected) {
160
153
  if (shutdown.stopping) return;
161
154
  if (!ready) throw new Error(`server did not become ready at ${plan.readyUrl}`);
162
155
  updatePidRecordState(plan, expected, "running");
163
- console.log(`[pievo-server] ready at http://${plan.host}:${plan.port} (pid ${process.pid})`);
156
+ console.log(`[pievo-server] ready on ${plan.host}:${plan.port} (pid ${process.pid})`);
164
157
  } catch (error) {
165
158
  // Once Nitro may own a listening socket, ask its srvx signal handler to drain
166
159
  // HTTP rather than forcing process.exit from our application handler.
@@ -196,6 +189,7 @@ async function waitForDetachedReady(plan, expected) {
196
189
  }
197
190
 
198
191
  async function cleanFailedLaunch(plan, expected) {
192
+ retractHttps(expected);
199
193
  const state = recordedProcessState(expected);
200
194
  if (state.state === "gone") {
201
195
  clearPidRecord(plan.pidFile, expected);
@@ -212,7 +206,7 @@ async function cleanFailedLaunch(plan, expected) {
212
206
  async function start(basePlan, foreground) {
213
207
  const existing = pidStatus(basePlan.pidFile);
214
208
  if (existing.state === "running") {
215
- console.log(`pievo-server is already ${existing.record.state === "starting" ? "starting" : "running"} (pid ${existing.record.pid}, http://${existing.record.host}:${existing.record.port})`);
209
+ console.log(`pievo server is already ${existing.record.state === "starting" ? "starting" : "running"} (pid ${existing.record.pid}); run \`pievo server status\` for its URLs`);
216
210
  return;
217
211
  }
218
212
  if (existing.state === "unsafe") throw new Error(`${existing.error}; refusing to start over the existing pid record`);
@@ -221,13 +215,15 @@ async function start(basePlan, foreground) {
221
215
  try {
222
216
  const raced = pidStatus(basePlan.pidFile);
223
217
  if (raced.state === "running") {
224
- console.log(`pievo-server is already ${raced.record.state === "starting" ? "starting" : "running"} (pid ${raced.record.pid})`);
218
+ console.log(`pievo server is already ${raced.record.state === "starting" ? "starting" : "running"} (pid ${raced.record.pid})`);
225
219
  return;
226
220
  }
227
221
  if (raced.state === "unsafe") throw new Error(`${raced.error}; refusing to start over the existing pid record`);
228
222
 
229
- const plan = withLaunchNonce(basePlan);
223
+ const published = publishNetwork(basePlan);
224
+ const plan = withLaunchNonce(published.plan);
230
225
  if (foreground) {
226
+ printUrls(published.urls, published.httpsNote);
231
227
  applyProcessEnv(plan.env);
232
228
  const record = writePidRecord(plan, {
233
229
  launchNonce: plan.launchNonce,
@@ -274,8 +270,9 @@ async function start(basePlan, foreground) {
274
270
  }
275
271
  throw error;
276
272
  }
277
- console.log(`pievo-server started (pid ${child.pid}, http://${plan.host}:${plan.port})`);
278
- console.log(`log: ${plan.logFile}`);
273
+ console.log(`pievo server started (pid ${child.pid})`);
274
+ printUrls(published.urls, published.httpsNote);
275
+ console.log(` log ${plan.logFile}`);
279
276
  } finally {
280
277
  release();
281
278
  }
@@ -319,8 +316,10 @@ async function stop(plan) {
319
316
  clearPidRecord(plan.pidFile, stoppedRecord);
320
317
  }
321
318
 
322
- if (stoppedRecord) console.log(`pievo-server stopped (pid ${stoppedRecord.pid})`);
323
- else console.log("pievo-server is not running");
319
+ if (stoppedRecord) {
320
+ retractHttps(stoppedRecord);
321
+ console.log(`pievo server stopped (pid ${stoppedRecord.pid})`);
322
+ } else console.log("pievo server is not running");
324
323
  } finally {
325
324
  release?.();
326
325
  }
@@ -329,17 +328,20 @@ async function stop(plan) {
329
328
  function status(plan) {
330
329
  const current = pidStatus(plan.pidFile);
331
330
  if (current.state === "running") {
332
- console.log(`pievo-server is ${current.record.state === "starting" ? "starting" : "running"} (pid ${current.record.pid})`);
333
- console.log(`url: http://${current.record.host}:${current.record.port}`);
334
- console.log(`data: ${plan.dataDir}`);
331
+ const { host, port } = current.record;
332
+ const tailscale = tailscaleInfo();
333
+ const https = tailscale ? currentHttpsUrl(tailscale, port) : undefined;
334
+ console.log(`pievo server is ${current.record.state === "starting" ? "starting" : "running"} (pid ${current.record.pid})`);
335
+ printUrls(reachableUrls({ host, port, https, tailscale }));
336
+ console.log(` data ${plan.dataDir}`);
335
337
  return 0;
336
338
  }
337
339
  if (current.state === "unsafe") {
338
340
  const pid = current.record?.pid ? ` (pid ${current.record.pid})` : "";
339
- console.error(`pievo-server status is unsafe: ${current.error}${pid}`);
341
+ console.error(`pievo server status is unsafe: ${current.error}${pid}`);
340
342
  return 1;
341
343
  }
342
- console.log(`pievo-server is stopped${current.stale ? " (cleared stale pid file)" : ""}`);
344
+ console.log(`pievo server is stopped${current.stale ? " (cleared stale pid file)" : ""}`);
343
345
  return 1;
344
346
  }
345
347
 
@@ -4,30 +4,32 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
- export const DEFAULT_HOST = "127.0.0.1";
8
- export const DEFAULT_PORT = 3000;
7
+ const DEFAULT_HOST = "0.0.0.0";
8
+ const DEFAULT_PORT = 3000;
9
9
  const MALFORMED_LOCK_GRACE_MS = 5_000;
10
10
 
11
11
  export function usage() {
12
- return `Usage: pievo-server <start|status|restart|stop> [options]
12
+ return `Usage: pievo server <start|status|restart|stop> [options]
13
13
 
14
14
  Commands:
15
15
  start start in the background (idempotent)
16
16
  start --foreground run in the foreground
17
- status show local process status
17
+ status show process status and every URL the server is reachable at
18
18
  restart stop, then start the currently installed server
19
19
  stop gracefully stop the local server
20
20
 
21
21
  Options:
22
- --host <host> bind host (default: HOST/NITRO_HOST or 127.0.0.1)
22
+ --host <host> bind host (default: HOST/NITRO_HOST or 0.0.0.0)
23
23
  --port <port> bind port (default: PORT/NITRO_PORT/PIEVO_PORT or 3000)
24
+ --no-https do not publish https://<device>.ts.net via Tailscale Serve
24
25
  --data-dir <path> select the instance (database, blobs, pid, lock, and log)
25
26
  -h, --help show this help
26
27
  -v, --version print the server version and exit
27
28
 
28
- Restart preserves the recorded instance host/port unless a flag or bind environment
29
- variable overrides it. The default bind is local-only. Set --host 0.0.0.0 only with
30
- appropriate auth and network controls.`;
29
+ The server accepts requests only from this machine, private LAN addresses, and
30
+ Tailscale peers. When Tailscale is running, start also publishes the dashboard at
31
+ https://<device>.ts.net (tailnet-only). Restart preserves the recorded host, port,
32
+ and HTTPS choice unless a flag or bind environment variable overrides it.`;
31
33
  }
32
34
 
33
35
  export function parseArgs(argv) {
@@ -44,6 +46,10 @@ export function parseArgs(argv) {
44
46
  out.foreground = true;
45
47
  continue;
46
48
  }
49
+ if (arg === "--no-https" && (command === "start" || command === "restart")) {
50
+ out.https = false;
51
+ continue;
52
+ }
47
53
  const match = arg.match(/^--(host|port|data-dir)(?:=(.*))?$/);
48
54
  if (match) {
49
55
  const key = match[1] === "data-dir" ? "dataDir" : match[1];
@@ -74,7 +80,7 @@ function firstSet(env, names) {
74
80
  }
75
81
 
76
82
  export function buildEnvPlan(options, baseEnv = process.env) {
77
- const dataDir = path.resolve(options.dataDir || baseEnv.PIEVO_DATA_DIR?.trim() || path.join(os.homedir(), ".pievo"));
83
+ const dataDir = path.resolve(options.dataDir || baseEnv.PIEVO_DATA_DIR?.trim() || path.join(os.homedir(), ".pievo-server"));
78
84
  const host = String(options.host || firstSet(baseEnv, ["NITRO_HOST", "HOST"]) || DEFAULT_HOST).trim();
79
85
  if (!host) throw new Error("host cannot be empty");
80
86
  const port = validPort(options.port || firstSet(baseEnv, ["NITRO_PORT", "PORT", "PIEVO_PORT"]) || DEFAULT_PORT);
@@ -88,12 +94,12 @@ export function buildEnvPlan(options, baseEnv = process.env) {
88
94
  PORT: String(port),
89
95
  };
90
96
  delete env.PIEVO_SERVER_LAUNCH_NONCE;
91
- if (!env.DATABASE_URL && !env.PIEVO_DB) env.PIEVO_DB = "pglite";
92
97
  return {
93
98
  env,
94
99
  dataDir,
95
100
  host,
96
101
  port,
102
+ https: options.https !== false,
97
103
  pidFile: path.join(dataDir, "server.pid"),
98
104
  lockFile: path.join(dataDir, "server.start.lock"),
99
105
  logFile: path.join(dataDir, "server.log"),
@@ -102,7 +108,7 @@ export function buildEnvPlan(options, baseEnv = process.env) {
102
108
  }
103
109
 
104
110
  /** Replace the current process environment with the launch plan. Foreground
105
- * starts need the same bind/database/nonce environment a detached child gets. */
111
+ * starts need the same bind/data-dir/nonce environment a detached child gets. */
106
112
  export function applyProcessEnv(nextEnv, currentEnv = process.env) {
107
113
  for (const key of Object.keys(currentEnv)) {
108
114
  if (!(key in nextEnv)) delete currentEnv[key];
@@ -118,6 +124,7 @@ export function buildRestartPlan(options, baseEnv, record) {
118
124
  if (preserved.port === undefined && firstSet(baseEnv, ["NITRO_PORT", "PORT", "PIEVO_PORT"]) === undefined && record?.port) {
119
125
  preserved.port = String(record.port);
120
126
  }
127
+ if (preserved.https === undefined && record?.https === false) preserved.https = false;
121
128
  return buildEnvPlan(preserved, baseEnv);
122
129
  }
123
130
 
@@ -168,7 +175,8 @@ function validPidRecord(value) {
168
175
  && ['starting', 'running'].includes(value.state)
169
176
  && typeof value.launchNonce === "string"
170
177
  && !!value.launchNonce
171
- && typeof value.managedGroup === "boolean";
178
+ && typeof value.managedGroup === "boolean"
179
+ && (value.https === undefined || typeof value.https === "boolean");
172
180
  }
173
181
 
174
182
  function readPidResult(pidFile) {
@@ -219,6 +227,7 @@ export function writePidRecord(plan, options = {}) {
219
227
  startTime,
220
228
  host: plan.host,
221
229
  port: plan.port,
230
+ https: plan.https,
222
231
  state: options.state ?? "starting",
223
232
  launchNonce: options.launchNonce,
224
233
  managedGroup: options.managedGroup ?? false,