@specific.dev/cli 0.1.126 → 0.1.128

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 (67) hide show
  1. package/dist/admin/404/index.html +1 -1
  2. package/dist/admin/404.html +1 -1
  3. package/dist/admin/__next.!KGRlZmF1bHQp.__PAGE__.txt +1 -1
  4. package/dist/admin/__next.!KGRlZmF1bHQp.txt +1 -1
  5. package/dist/admin/__next._full.txt +1 -1
  6. package/dist/admin/__next._head.txt +1 -1
  7. package/dist/admin/__next._index.txt +1 -1
  8. package/dist/admin/__next._tree.txt +1 -1
  9. package/dist/admin/_not-found/__next._full.txt +1 -1
  10. package/dist/admin/_not-found/__next._head.txt +1 -1
  11. package/dist/admin/_not-found/__next._index.txt +1 -1
  12. package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
  13. package/dist/admin/_not-found/__next._not-found.txt +1 -1
  14. package/dist/admin/_not-found/__next._tree.txt +1 -1
  15. package/dist/admin/_not-found/index.html +1 -1
  16. package/dist/admin/_not-found/index.txt +1 -1
  17. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +1 -1
  18. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
  19. package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +1 -1
  20. package/dist/admin/databases/__next._full.txt +1 -1
  21. package/dist/admin/databases/__next._head.txt +1 -1
  22. package/dist/admin/databases/__next._index.txt +1 -1
  23. package/dist/admin/databases/__next._tree.txt +1 -1
  24. package/dist/admin/databases/index.html +1 -1
  25. package/dist/admin/databases/index.txt +1 -1
  26. package/dist/admin/fullscreen/__next._full.txt +1 -1
  27. package/dist/admin/fullscreen/__next._head.txt +1 -1
  28. package/dist/admin/fullscreen/__next._index.txt +1 -1
  29. package/dist/admin/fullscreen/__next._tree.txt +1 -1
  30. package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +1 -1
  31. package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
  32. package/dist/admin/fullscreen/databases/__next._full.txt +1 -1
  33. package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
  34. package/dist/admin/fullscreen/databases/__next._index.txt +1 -1
  35. package/dist/admin/fullscreen/databases/__next._tree.txt +1 -1
  36. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +1 -1
  37. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.txt +1 -1
  38. package/dist/admin/fullscreen/databases/__next.fullscreen.txt +1 -1
  39. package/dist/admin/fullscreen/databases/index.html +1 -1
  40. package/dist/admin/fullscreen/databases/index.txt +1 -1
  41. package/dist/admin/fullscreen/index.html +1 -1
  42. package/dist/admin/fullscreen/index.txt +1 -1
  43. package/dist/admin/index.html +1 -1
  44. package/dist/admin/index.txt +1 -1
  45. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +1 -1
  46. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +1 -1
  47. package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +1 -1
  48. package/dist/admin/mail/__next._full.txt +1 -1
  49. package/dist/admin/mail/__next._head.txt +1 -1
  50. package/dist/admin/mail/__next._index.txt +1 -1
  51. package/dist/admin/mail/__next._tree.txt +1 -1
  52. package/dist/admin/mail/index.html +1 -1
  53. package/dist/admin/mail/index.txt +1 -1
  54. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +1 -1
  55. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +1 -1
  56. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
  57. package/dist/admin/workflows/__next._full.txt +1 -1
  58. package/dist/admin/workflows/__next._head.txt +1 -1
  59. package/dist/admin/workflows/__next._index.txt +1 -1
  60. package/dist/admin/workflows/__next._tree.txt +1 -1
  61. package/dist/admin/workflows/index.html +1 -1
  62. package/dist/admin/workflows/index.txt +1 -1
  63. package/dist/cli.js +424 -91
  64. package/package.json +4 -4
  65. /package/dist/admin/_next/static/{xkQQHpz09QHzHXw5WkFYT → RI2b-FbUxlHXeowkJKNZM}/_buildManifest.js +0 -0
  66. /package/dist/admin/_next/static/{xkQQHpz09QHzHXw5WkFYT → RI2b-FbUxlHXeowkJKNZM}/_clientMiddlewareManifest.json +0 -0
  67. /package/dist/admin/_next/static/{xkQQHpz09QHzHXw5WkFYT → RI2b-FbUxlHXeowkJKNZM}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -185179,25 +185179,26 @@ var chokidar_default = { watch, FSWatcher };
185179
185179
  var import_code_frame = __toESM(require_lib2(), 1);
185180
185180
  import * as fs5 from "fs";
185181
185181
  import * as path5 from "path";
185182
- import * as net from "net";
185182
+ import * as net2 from "net";
185183
185183
  import { spawn } from "child_process";
185184
185184
  import * as fs4 from "fs";
185185
185185
  import * as path4 from "path";
185186
185186
  import * as os2 from "os";
185187
185187
  import { createReadStream } from "fs";
185188
185188
  import { createTarExtractor, extractTo } from "tar-vern";
185189
+ import * as net from "net";
185190
+ import { execFile } from "child_process";
185191
+ import { promisify } from "util";
185189
185192
  import { spawn as spawn2 } from "child_process";
185190
185193
  import { readFile, writeFile } from "fs/promises";
185191
185194
  import { existsSync as existsSync5 } from "fs";
185192
185195
  import * as fs6 from "fs";
185193
185196
  import * as path6 from "path";
185194
- import { execFile } from "child_process";
185195
- import { promisify } from "util";
185196
185197
  import * as http from "http";
185197
185198
  import * as fs7 from "fs";
185198
185199
  import * as path7 from "path";
185199
185200
  import { fileURLToPath } from "url";
185200
- import * as net2 from "net";
185201
+ import * as net3 from "net";
185201
185202
  import * as fs8 from "fs";
185202
185203
  import * as path9 from "path";
185203
185204
  import { spawn as spawn3 } from "child_process";
@@ -185207,7 +185208,7 @@ import * as path8 from "path";
185207
185208
  import * as crypto from "crypto";
185208
185209
  import * as http2 from "http";
185209
185210
  import * as crypto2 from "crypto";
185210
- import * as net3 from "net";
185211
+ import * as net4 from "net";
185211
185212
  import * as fs9 from "fs";
185212
185213
  import * as path10 from "path";
185213
185214
  import { spawn as spawn4 } from "child_process";
@@ -185217,14 +185218,14 @@ import * as path11 from "path";
185217
185218
  import { spawnSync } from "child_process";
185218
185219
  import * as fs11 from "fs";
185219
185220
  import * as path12 from "path";
185220
- import * as net4 from "net";
185221
- import { spawn as spawn5 } from "child_process";
185222
185221
  import * as net5 from "net";
185222
+ import { spawn as spawn5 } from "child_process";
185223
+ import * as net6 from "net";
185223
185224
  import * as fs12 from "fs";
185224
185225
  import * as path13 from "path";
185225
185226
  import { generateSlug } from "random-word-slugs";
185226
185227
  import { EventEmitter as EventEmitter2 } from "node:events";
185227
- import * as net6 from "node:net";
185228
+ import * as net7 from "node:net";
185228
185229
  import { EventEmitter as EventEmitter22 } from "events";
185229
185230
  import { execSync as execSync2 } from "child_process";
185230
185231
  import * as path15 from "path";
@@ -369275,14 +369276,22 @@ var temporalBinary = {
369275
369276
  stripComponents: 0,
369276
369277
  executables: ["temporal"]
369277
369278
  };
369278
- function killProcess(proc) {
369279
+ function killProcess(proc, opts = {}) {
369279
369280
  return new Promise((resolve52) => {
369280
369281
  if (proc.killed || proc.exitCode !== null) {
369281
369282
  resolve52();
369282
369283
  return;
369283
369284
  }
369284
369285
  proc.once("exit", () => resolve52());
369285
- proc.kill("SIGKILL");
369286
+ if (opts.detached && proc.pid) {
369287
+ try {
369288
+ process.kill(-proc.pid, "SIGKILL");
369289
+ } catch {
369290
+ proc.kill("SIGKILL");
369291
+ }
369292
+ } else {
369293
+ proc.kill("SIGKILL");
369294
+ }
369286
369295
  });
369287
369296
  }
369288
369297
  function killProcessGroup(pid) {
@@ -369291,6 +369300,143 @@ function killProcessGroup(pid) {
369291
369300
  } catch {
369292
369301
  }
369293
369302
  }
369303
+ var execFileAsync = promisify(execFile);
369304
+ function isPortInUse(host, port) {
369305
+ return new Promise((resolve52) => {
369306
+ const socket = new net.Socket();
369307
+ socket.setTimeout(1e3);
369308
+ socket.on("connect", () => {
369309
+ socket.destroy();
369310
+ resolve52(true);
369311
+ });
369312
+ socket.on("timeout", () => {
369313
+ socket.destroy();
369314
+ resolve52(false);
369315
+ });
369316
+ socket.on("error", () => {
369317
+ socket.destroy();
369318
+ resolve52(false);
369319
+ });
369320
+ socket.connect(port, host);
369321
+ });
369322
+ }
369323
+ async function waitForPortFree(host, port, timeoutMs = 3e3) {
369324
+ const deadline = Date.now() + timeoutMs;
369325
+ while (Date.now() < deadline) {
369326
+ if (!await isPortInUse(host, port)) return true;
369327
+ await new Promise((r) => setTimeout(r, 100));
369328
+ }
369329
+ return !await isPortInUse(host, port);
369330
+ }
369331
+ async function getListenersOnPort(port) {
369332
+ let pids = [];
369333
+ try {
369334
+ const { stdout } = await execFileAsync(
369335
+ "lsof",
369336
+ ["-i", `:${port}`, "-sTCP:LISTEN", "-t"],
369337
+ { timeout: 5e3 }
369338
+ );
369339
+ pids = stdout.trim().split("\n").map((l) => parseInt(l.trim(), 10)).filter((n) => !isNaN(n));
369340
+ } catch {
369341
+ return [];
369342
+ }
369343
+ const results = [];
369344
+ for (const pid of pids) {
369345
+ results.push({ pid, command: await resolveCommand(pid) });
369346
+ }
369347
+ return results;
369348
+ }
369349
+ async function resolveCommand(pid) {
369350
+ try {
369351
+ const { stdout } = await execFileAsync(
369352
+ "ps",
369353
+ ["-p", String(pid), "-o", "command="],
369354
+ { timeout: 1e3 }
369355
+ );
369356
+ return stdout.trim();
369357
+ } catch {
369358
+ return "";
369359
+ }
369360
+ }
369361
+ async function reclaimSpecificOrphanOnPort(port) {
369362
+ const listeners = await getListenersOnPort(port);
369363
+ let killed = false;
369364
+ for (const { pid, command } of listeners) {
369365
+ if (!isOwnedBySpecific(command)) continue;
369366
+ try {
369367
+ process.kill(-pid, "SIGKILL");
369368
+ killed = true;
369369
+ } catch {
369370
+ try {
369371
+ process.kill(pid, "SIGKILL");
369372
+ killed = true;
369373
+ } catch {
369374
+ }
369375
+ }
369376
+ }
369377
+ return killed;
369378
+ }
369379
+ function isOwnedBySpecific(argv) {
369380
+ return argv.includes("/.specific/bin/") || argv.includes("/.burrito/electric_");
369381
+ }
369382
+ async function killSpecificProcessesReferencing(needle) {
369383
+ if (!needle) return 0;
369384
+ let stdout = "";
369385
+ try {
369386
+ const result = await execFileAsync("ps", ["-A", "-o", "pid=,command="], {
369387
+ timeout: 5e3,
369388
+ maxBuffer: 10 * 1024 * 1024
369389
+ });
369390
+ stdout = result.stdout;
369391
+ } catch {
369392
+ return 0;
369393
+ }
369394
+ const self2 = process.pid;
369395
+ const candidates = [];
369396
+ for (const raw of stdout.split("\n")) {
369397
+ const line = raw.trim();
369398
+ if (!line) continue;
369399
+ const space = line.indexOf(" ");
369400
+ if (space <= 0) continue;
369401
+ const pid = parseInt(line.slice(0, space), 10);
369402
+ if (isNaN(pid) || pid === self2) continue;
369403
+ const command = line.slice(space + 1);
369404
+ if (!isOwnedBySpecific(command)) continue;
369405
+ if (command.includes(needle) || await processOpensPathUnder(pid, needle)) {
369406
+ candidates.push(pid);
369407
+ }
369408
+ }
369409
+ let killed = 0;
369410
+ for (const pid of candidates) {
369411
+ try {
369412
+ process.kill(-pid, "SIGKILL");
369413
+ killed++;
369414
+ } catch {
369415
+ try {
369416
+ process.kill(pid, "SIGKILL");
369417
+ killed++;
369418
+ } catch {
369419
+ }
369420
+ }
369421
+ }
369422
+ return killed;
369423
+ }
369424
+ async function processOpensPathUnder(pid, prefix) {
369425
+ try {
369426
+ const { stdout } = await execFileAsync("lsof", ["-p", String(pid), "-F", "n"], {
369427
+ timeout: 3e3,
369428
+ maxBuffer: 5 * 1024 * 1024
369429
+ });
369430
+ for (const raw of stdout.split("\n")) {
369431
+ if (raw.length > 1 && raw[0] === "n" && raw.slice(1).startsWith(prefix)) {
369432
+ return true;
369433
+ }
369434
+ }
369435
+ } catch {
369436
+ return false;
369437
+ }
369438
+ return false;
369439
+ }
369294
369440
  async function startPostgres(pg, port, dataDir, onProgress) {
369295
369441
  const binary = await ensureBinary(postgresBinary, void 0, onProgress);
369296
369442
  const dbDataPath = path5.join(process.cwd(), dataDir, pg.name);
@@ -369299,6 +369445,7 @@ async function startPostgres(pg, port, dataDir, onProgress) {
369299
369445
  const password = "postgres";
369300
369446
  const libraryEnv = getLibraryEnv(binary);
369301
369447
  const env2 = { ...process.env, ...libraryEnv };
369448
+ reclaimOrphanedPostmaster(dbDataPath);
369302
369449
  const pgConfPath = path5.join(dbDataPath, "postgresql.conf");
369303
369450
  const dataValid = fs5.existsSync(pgConfPath);
369304
369451
  if (!dataValid) {
@@ -369314,6 +369461,10 @@ async function startPostgres(pg, port, dataDir, onProgress) {
369314
369461
  } catch {
369315
369462
  }
369316
369463
  }
369464
+ if (await isPortInUse(host, port)) {
369465
+ await reclaimSpecificOrphanOnPort(port);
369466
+ await waitForPortFree(host, port, 1e3);
369467
+ }
369317
369468
  const postgres = spawn(
369318
369469
  binary.executables["postgres"],
369319
369470
  [
@@ -369332,9 +369483,11 @@ async function startPostgres(pg, port, dataDir, onProgress) {
369332
369483
  ],
369333
369484
  {
369334
369485
  stdio: ["ignore", "pipe", "pipe"],
369335
- env: env2
369486
+ env: env2,
369487
+ detached: true
369336
369488
  }
369337
369489
  );
369490
+ postgres.unref();
369338
369491
  pipeProcess("postgres", postgres);
369339
369492
  await Promise.race([
369340
369493
  waitForTcpPort(host, port),
@@ -369358,21 +369511,28 @@ async function startPostgres(pg, port, dataDir, onProgress) {
369358
369511
  dbName: pg.name,
369359
369512
  url: `postgres://${user}:${password}@${host}:${port}/${pg.name}`,
369360
369513
  pid: postgres.pid,
369514
+ detached: true,
369361
369515
  async stop() {
369362
- return killProcess(postgres);
369516
+ return killProcess(postgres, { detached: true });
369363
369517
  }
369364
369518
  };
369365
369519
  }
369366
369520
  async function startRedis(redis, port, onProgress) {
369367
369521
  const binary = await ensureBinary(redisBinary, void 0, onProgress);
369368
369522
  const host = "127.0.0.1";
369523
+ if (await isPortInUse(host, port)) {
369524
+ await reclaimSpecificOrphanOnPort(port);
369525
+ await waitForPortFree(host, port, 1e3);
369526
+ }
369369
369527
  const redisProc = spawn(
369370
369528
  binary.executables["redis-server"],
369371
369529
  ["--port", String(port), "--bind", host, "--daemonize", "no"],
369372
369530
  {
369373
- stdio: ["ignore", "pipe", "pipe"]
369531
+ stdio: ["ignore", "pipe", "pipe"],
369532
+ detached: true
369374
369533
  }
369375
369534
  );
369535
+ redisProc.unref();
369376
369536
  pipeProcess("redis", redisProc);
369377
369537
  await waitForTcpPort(host, port);
369378
369538
  return {
@@ -369385,8 +369545,9 @@ async function startRedis(redis, port, onProgress) {
369385
369545
  dbName: redis.name,
369386
369546
  url: `redis://${host}:${port}`,
369387
369547
  pid: redisProc.pid,
369548
+ detached: true,
369388
369549
  async stop() {
369389
- return killProcess(redisProc);
369550
+ return killProcess(redisProc, { detached: true });
369390
369551
  }
369391
369552
  };
369392
369553
  }
@@ -369474,6 +369635,39 @@ async function createPostgresDatabase(postgresPath, dataDir, dbName, env2) {
369474
369635
  proc.stdin?.end();
369475
369636
  });
369476
369637
  }
369638
+ function reclaimOrphanedPostmaster(dbDataPath) {
369639
+ const pidFile = path5.join(dbDataPath, "postmaster.pid");
369640
+ if (!fs5.existsSync(pidFile)) return;
369641
+ let pid;
369642
+ try {
369643
+ const firstLine = fs5.readFileSync(pidFile, "utf-8").split("\n")[0] ?? "";
369644
+ pid = parseInt(firstLine.trim(), 10);
369645
+ } catch {
369646
+ return;
369647
+ }
369648
+ if (isNaN(pid) || pid <= 0) return;
369649
+ let alive = false;
369650
+ try {
369651
+ process.kill(pid, 0);
369652
+ alive = true;
369653
+ } catch (e) {
369654
+ alive = e.code !== "ESRCH";
369655
+ }
369656
+ if (alive) {
369657
+ try {
369658
+ process.kill(-pid, "SIGKILL");
369659
+ } catch {
369660
+ try {
369661
+ process.kill(pid, "SIGKILL");
369662
+ } catch {
369663
+ }
369664
+ }
369665
+ }
369666
+ try {
369667
+ fs5.unlinkSync(pidFile);
369668
+ } catch {
369669
+ }
369670
+ }
369477
369671
  async function waitForTcpPort(host, port, timeoutMs = 3e4) {
369478
369672
  const startTime = Date.now();
369479
369673
  while (Date.now() - startTime < timeoutMs) {
@@ -369487,7 +369681,7 @@ async function waitForTcpPort(host, port, timeoutMs = 3e4) {
369487
369681
  }
369488
369682
  function checkTcpPort(host, port) {
369489
369683
  return new Promise((resolve52) => {
369490
- const socket = new net.Socket();
369684
+ const socket = new net2.Socket();
369491
369685
  socket.setTimeout(1e3);
369492
369686
  socket.on("connect", () => {
369493
369687
  socket.destroy();
@@ -370015,7 +370209,6 @@ function startService(service, resources, secrets, configs, endpointPorts, servi
370015
370209
  }
370016
370210
  };
370017
370211
  }
370018
- var execFileAsync = promisify(execFile);
370019
370212
  var InstanceStateManager = class {
370020
370213
  stateDir;
370021
370214
  statePath;
@@ -370107,8 +370300,20 @@ var InstanceStateManager = class {
370107
370300
  releaseLock();
370108
370301
  }
370109
370302
  }
370303
+ /**
370304
+ * Kill every Specific-owned process (postgres, redis, electric, temporal,
370305
+ * drizzle-gateway) whose command line references this key's data
370306
+ * directory. This is the safety net for when state.json is missing or
370307
+ * incomplete — the port allocator can pick a different block on restart,
370308
+ * so port-based cleanup alone isn't enough. We identify orphans by their
370309
+ * data-dir/config-dir argument, which is stable across runs.
370310
+ */
370311
+ async sweepKeyDirOrphans() {
370312
+ await killSpecificProcessesReferencing(this.stateDir);
370313
+ }
370110
370314
  async cleanStaleState() {
370111
370315
  if (!fs6.existsSync(this.statePath)) {
370316
+ await this.sweepKeyDirOrphans();
370112
370317
  return false;
370113
370318
  }
370114
370319
  const releaseLock = await this.acquireLock();
@@ -370117,6 +370322,7 @@ var InstanceStateManager = class {
370117
370322
  const state = JSON.parse(content);
370118
370323
  if (!this.isProcessRunning(state.owner.pid)) {
370119
370324
  await this.killOrphanedProcesses(state);
370325
+ await this.sweepKeyDirOrphans();
370120
370326
  fs6.unlinkSync(this.statePath);
370121
370327
  return true;
370122
370328
  }
@@ -370146,18 +370352,22 @@ var InstanceStateManager = class {
370146
370352
  }
370147
370353
  }
370148
370354
  for (const db of Object.values(state.databases)) {
370149
- if (db.syncUrl) {
370150
- try {
370151
- const url = new URL(db.syncUrl);
370152
- const port = parseInt(url.port, 10);
370153
- if (!isNaN(port)) {
370154
- const pid = await findPidOnPort(port);
370155
- if (pid !== void 0) {
370156
- process.kill(pid, "SIGKILL");
370157
- }
370158
- }
370159
- } catch {
370160
- }
370355
+ killTrackedProcess(db.pid, db.detached, this.isProcessRunning.bind(this));
370356
+ if (db.port) {
370357
+ await reclaimSpecificOrphanOnPort(db.port);
370358
+ }
370359
+ }
370360
+ for (const aux of Object.values(state.electric ?? {})) {
370361
+ killTrackedProcess(aux.pid, aux.detached, this.isProcessRunning.bind(this));
370362
+ if (aux.port) {
370363
+ await reclaimSpecificOrphanOnPort(aux.port);
370364
+ }
370365
+ }
370366
+ if (state.drizzleGateway) {
370367
+ const dg = state.drizzleGateway;
370368
+ killTrackedProcess(dg.pid, dg.detached, this.isProcessRunning.bind(this));
370369
+ if (dg.port) {
370370
+ await reclaimSpecificOrphanOnPort(dg.port);
370161
370371
  }
370162
370372
  }
370163
370373
  }
@@ -370228,6 +370438,32 @@ var InstanceStateManager = class {
370228
370438
  releaseLock();
370229
370439
  }
370230
370440
  }
370441
+ async registerElectric(databaseName, info) {
370442
+ if (!this.ownsInstances) {
370443
+ throw new Error("Cannot register Electric: not the owner");
370444
+ }
370445
+ const releaseLock = await this.acquireLock();
370446
+ try {
370447
+ const state = this.readState();
370448
+ state.electric = { ...state.electric ?? {}, [databaseName]: info };
370449
+ this.writeStateAtomic(state);
370450
+ } finally {
370451
+ releaseLock();
370452
+ }
370453
+ }
370454
+ async registerDrizzleGateway(info) {
370455
+ if (!this.ownsInstances) {
370456
+ throw new Error("Cannot register Drizzle Gateway: not the owner");
370457
+ }
370458
+ const releaseLock = await this.acquireLock();
370459
+ try {
370460
+ const state = this.readState();
370461
+ state.drizzleGateway = info;
370462
+ this.writeStateAtomic(state);
370463
+ } finally {
370464
+ releaseLock();
370465
+ }
370466
+ }
370231
370467
  async setPublicUrls(publicUrls) {
370232
370468
  if (!this.ownsInstances) {
370233
370469
  throw new Error("Cannot set public URLs: not the owner");
@@ -370266,17 +370502,15 @@ var InstanceStateManager = class {
370266
370502
  fs6.renameSync(tmpPath, this.statePath);
370267
370503
  }
370268
370504
  };
370269
- async function findPidOnPort(port) {
370505
+ function killTrackedProcess(pid, detached, isProcessRunning) {
370506
+ if (!pid || !isProcessRunning(pid)) return;
370270
370507
  try {
370271
- const { stdout } = await execFileAsync(
370272
- "lsof",
370273
- ["-i", `:${port}`, "-t", "-sTCP:LISTEN"],
370274
- { timeout: 5e3 }
370275
- );
370276
- const pid = parseInt(stdout.trim().split("\n")[0], 10);
370277
- return isNaN(pid) ? void 0 : pid;
370508
+ if (detached) {
370509
+ process.kill(-pid, "SIGKILL");
370510
+ } else {
370511
+ process.kill(pid, "SIGKILL");
370512
+ }
370278
370513
  } catch {
370279
- return void 0;
370280
370514
  }
370281
370515
  }
370282
370516
  var __dirname = path7.dirname(fileURLToPath(import.meta.url));
@@ -370574,21 +370808,15 @@ async function startElectric(postgres, port, dataDir, options2) {
370574
370808
  );
370575
370809
  const secret = generateRandomString(32);
370576
370810
  const host = "127.0.0.1";
370577
- if (await checkTcpPort2(host, port)) {
370578
- let freed = false;
370579
- for (let i = 0; i < 30; i++) {
370580
- await sleep2(100);
370581
- if (!await checkTcpPort2(host, port)) {
370582
- freed = true;
370583
- break;
370584
- }
370585
- }
370811
+ if (await isPortInUse(host, port)) {
370812
+ await reclaimSpecificOrphanOnPort(port);
370813
+ const freed = await waitForPortFree(host, port, 3e3);
370586
370814
  if (!freed) {
370587
370815
  throw new Error(
370588
- `Electric port ${port} is already in use. This may be an orphaned process from a previous session \u2014 find it with \`lsof -i :${port}\` and kill it, then retry.`
370816
+ `Electric port ${port} is already in use by a non-Specific process. Find it with \`lsof -i :${port}\` and kill it, then retry.`
370589
370817
  );
370590
370818
  }
370591
- writeLog("electric", `Port ${port} was occupied but is now free`);
370819
+ writeLog("electric", `Port ${port} was occupied but has been reclaimed`);
370592
370820
  }
370593
370821
  const storageDir = path9.join(process.cwd(), dataDir, `electric-${postgres.name}`);
370594
370822
  fs8.rmSync(storageDir, { recursive: true, force: true });
@@ -370600,6 +370828,7 @@ async function startElectric(postgres, port, dataDir, options2) {
370600
370828
  writeLog("electric", `Binary: ${binary.executables["electric"]}`);
370601
370829
  const electric = spawn3(binary.executables["electric"], [], {
370602
370830
  stdio: ["ignore", "pipe", "pipe"],
370831
+ detached: true,
370603
370832
  env: {
370604
370833
  ...process.env,
370605
370834
  DATABASE_URL: postgres.url,
@@ -370610,16 +370839,18 @@ async function startElectric(postgres, port, dataDir, options2) {
370610
370839
  ELECTRIC_STORAGE: "MEMORY"
370611
370840
  }
370612
370841
  });
370842
+ electric.unref();
370613
370843
  pipeProcess("electric", electric);
370614
370844
  await waitForTcpPort2(host, port);
370615
370845
  return {
370616
370846
  databaseName: postgres.name,
370617
370847
  pid: electric.pid,
370848
+ detached: true,
370618
370849
  port,
370619
370850
  url: `http://${host}:${port}`,
370620
370851
  secret,
370621
370852
  async stop() {
370622
- return killProcess(electric);
370853
+ return killProcess(electric, { detached: true });
370623
370854
  }
370624
370855
  };
370625
370856
  }
@@ -370636,7 +370867,7 @@ async function waitForTcpPort2(host, port, timeoutMs = 3e4) {
370636
370867
  }
370637
370868
  function checkTcpPort2(host, port) {
370638
370869
  return new Promise((resolve52) => {
370639
- const socket = new net2.Socket();
370870
+ const socket = new net3.Socket();
370640
370871
  socket.setTimeout(1e3);
370641
370872
  socket.on("connect", () => {
370642
370873
  socket.destroy();
@@ -370822,22 +371053,29 @@ async function startDrizzleGateway(postgresInstances, port, configDir, options2)
370822
371053
  "drizzle-gateway",
370823
371054
  `Binary: ${binary.executables["drizzle-gateway"]}`
370824
371055
  );
371056
+ if (await isPortInUse(host, port)) {
371057
+ await reclaimSpecificOrphanOnPort(port);
371058
+ await waitForPortFree(host, port, 1e3);
371059
+ }
370825
371060
  const drizzleGateway = spawn4(binary.executables["drizzle-gateway"], [], {
370826
371061
  stdio: ["ignore", "pipe", "pipe"],
371062
+ detached: true,
370827
371063
  env: {
370828
371064
  ...process.env,
370829
371065
  STORE_PATH: drizzleConfigDir,
370830
371066
  PORT: String(port)
370831
371067
  }
370832
371068
  });
371069
+ drizzleGateway.unref();
370833
371070
  pipeProcess("drizzle-gateway", drizzleGateway);
370834
371071
  await waitForTcpPort3(host, port);
370835
371072
  return {
370836
371073
  port,
370837
371074
  url: `http://${host}:${port}`,
370838
371075
  pid: drizzleGateway.pid,
371076
+ detached: true,
370839
371077
  async stop() {
370840
- return killProcess(drizzleGateway);
371078
+ return killProcess(drizzleGateway, { detached: true });
370841
371079
  }
370842
371080
  };
370843
371081
  }
@@ -370856,7 +371094,7 @@ async function waitForTcpPort3(host, port, timeoutMs = 3e4) {
370856
371094
  }
370857
371095
  function checkTcpPort3(host, port) {
370858
371096
  return new Promise((resolve52) => {
370859
- const socket = new net3.Socket();
371097
+ const socket = new net4.Socket();
370860
371098
  socket.setTimeout(1e3);
370861
371099
  socket.on("connect", () => {
370862
371100
  socket.destroy();
@@ -371190,6 +371428,12 @@ async function startTemporalDevServer(temporals, grpcPort, uiPort, dataDir, onPr
371190
371428
  }
371191
371429
  const host = "127.0.0.1";
371192
371430
  const namespaceArgs = temporals.flatMap((t) => ["--namespace", t.name]);
371431
+ for (const p of [grpcPort, uiPort]) {
371432
+ if (await isPortInUse(host, p)) {
371433
+ await reclaimSpecificOrphanOnPort(p);
371434
+ await waitForPortFree(host, p, 1e3);
371435
+ }
371436
+ }
371193
371437
  const proc = spawn5(
371194
371438
  binary.executables["temporal"],
371195
371439
  [
@@ -371208,12 +371452,14 @@ async function startTemporalDevServer(temporals, grpcPort, uiPort, dataDir, onPr
371208
371452
  "pretty"
371209
371453
  ],
371210
371454
  {
371211
- stdio: ["ignore", "pipe", "pipe"]
371455
+ stdio: ["ignore", "pipe", "pipe"],
371456
+ detached: true
371212
371457
  }
371213
371458
  );
371459
+ proc.unref();
371214
371460
  pipeProcess("temporal", proc);
371215
371461
  await waitForTcpPort4(host, grpcPort);
371216
- const stopServer = () => killProcess(proc);
371462
+ const stopServer = () => killProcess(proc, { detached: true });
371217
371463
  const instances = temporals.map((temporal, i) => ({
371218
371464
  name: temporal.name,
371219
371465
  type: "temporal",
@@ -371225,6 +371471,7 @@ async function startTemporalDevServer(temporals, grpcPort, uiPort, dataDir, onPr
371225
371471
  url: `${host}:${grpcPort}`,
371226
371472
  uiPort,
371227
371473
  pid: i === 0 ? proc.pid : void 0,
371474
+ detached: i === 0 ? true : void 0,
371228
371475
  // Only the first instance owns the server lifecycle
371229
371476
  stop: i === 0 ? stopServer : async () => {
371230
371477
  }
@@ -371244,7 +371491,7 @@ async function waitForTcpPort4(host, port, timeoutMs = 3e4) {
371244
371491
  }
371245
371492
  function checkTcpPort4(host, port) {
371246
371493
  return new Promise((resolve52) => {
371247
- const socket = new net4.Socket();
371494
+ const socket = new net5.Socket();
371248
371495
  socket.setTimeout(1e3);
371249
371496
  socket.on("connect", () => {
371250
371497
  socket.destroy();
@@ -371346,6 +371593,8 @@ async function startResources(options2) {
371346
371593
  dbName: instance.dbName,
371347
371594
  url: instance.url
371348
371595
  };
371596
+ if (instance.pid !== void 0) dbState.pid = instance.pid;
371597
+ if (instance.detached) dbState.detached = true;
371349
371598
  if (instance.reshapeEnabled) {
371350
371599
  dbState.reshapeEnabled = true;
371351
371600
  if (instance.reshapeSearchPath) {
@@ -371368,7 +371617,7 @@ async function startResources(options2) {
371368
371617
  startedResources.push(instance);
371369
371618
  callbacks.onResourceReady?.(redis.name, instance);
371370
371619
  log(`Redis "${redis.name}" ready`);
371371
- await stateManager.registerDatabase(redis.name, {
371620
+ const redisState = {
371372
371621
  engine: "redis",
371373
371622
  port: instance.port,
371374
371623
  host: instance.host,
@@ -371376,7 +371625,10 @@ async function startResources(options2) {
371376
371625
  password: instance.password,
371377
371626
  dbName: instance.dbName,
371378
371627
  url: instance.url
371379
- });
371628
+ };
371629
+ if (instance.pid !== void 0) redisState.pid = instance.pid;
371630
+ if (instance.detached) redisState.detached = true;
371631
+ await stateManager.registerDatabase(redis.name, redisState);
371380
371632
  }
371381
371633
  for (const storage of storageConfigs) {
371382
371634
  if (signal?.cancelled) {
@@ -371430,6 +371682,8 @@ async function startResources(options2) {
371430
371682
  dbName: "",
371431
371683
  url: instance.url
371432
371684
  };
371685
+ if (instance.pid !== void 0) dbState.pid = instance.pid;
371686
+ if (instance.detached) dbState.detached = true;
371433
371687
  await stateManager.registerDatabase(instance.name, dbState);
371434
371688
  }
371435
371689
  }
@@ -371478,6 +371732,13 @@ async function startResources(options2) {
371478
371732
  syncUrl: electricInstance.url,
371479
371733
  syncSecret: electricInstance.secret
371480
371734
  });
371735
+ if (electricInstance.pid !== void 0) {
371736
+ await stateManager.registerElectric(pgName, {
371737
+ port: electricInstance.port,
371738
+ pid: electricInstance.pid,
371739
+ detached: electricInstance.detached
371740
+ });
371741
+ }
371481
371742
  callbacks.onElectricReady?.(pgName, electricInstance);
371482
371743
  log(`Electric sync for "${pgName}" ready at ${electricInstance.url}`);
371483
371744
  }
@@ -371563,7 +371824,7 @@ var BlockPortAllocator = class _BlockPortAllocator {
371563
371824
  };
371564
371825
  function isPortAvailable(port) {
371565
371826
  return new Promise((resolve52) => {
371566
- const server = net5.createServer();
371827
+ const server = net6.createServer();
371567
371828
  server.once("error", () => resolve52(false));
371568
371829
  server.once("listening", () => {
371569
371830
  server.close(() => resolve52(true));
@@ -371677,41 +371938,80 @@ var TunnelClientImpl = class extends EventEmitter2 {
371677
371938
  }
371678
371939
  }
371679
371940
  addPoolConnection() {
371680
- const remote = net6.connect({ host: this.tunnelHost, port: this.info.port });
371941
+ const remote = net7.connect({ host: this.tunnelHost, port: this.info.port });
371681
371942
  remote.setKeepAlive(true, 3e4);
371682
371943
  this.pool.add(remote);
371944
+ let active = false;
371945
+ let errored = false;
371683
371946
  remote.once("data", (firstChunk) => {
371684
371947
  this.pool.delete(remote);
371948
+ active = true;
371685
371949
  this.pipeToLocal(remote, firstChunk);
371686
371950
  });
371687
- let errored = false;
371688
371951
  remote.on("error", (err) => {
371689
371952
  errored = true;
371690
371953
  this.emit("error", err);
371691
371954
  });
371692
- remote.on("close", () => this.onIdleClose(remote, errored));
371955
+ remote.on("close", () => {
371956
+ if (this.closed)
371957
+ return;
371958
+ if (active) {
371959
+ this.fillPool();
371960
+ return;
371961
+ }
371962
+ this.pool.delete(remote);
371963
+ if (errored && this.pool.size === 0) {
371964
+ this.reconnect();
371965
+ } else {
371966
+ this.fillPool();
371967
+ }
371968
+ });
371693
371969
  }
371694
371970
  pipeToLocal(remote, firstChunk) {
371695
- const local = net6.connect({ host: "127.0.0.1", port: this.localPort }, () => {
371696
- local.write(firstChunk);
371971
+ let buffered = firstChunk;
371972
+ const HEADER_END = Buffer.from("\r\n\r\n");
371973
+ const MAX_HEADER_BYTES = 64 * 1024;
371974
+ const flush = (end) => {
371975
+ const headers = buffered.subarray(0, end + HEADER_END.length);
371976
+ const body = buffered.subarray(end + HEADER_END.length);
371977
+ const rewritten = this.rewriteDevOriginHeaders(headers);
371978
+ const request2 = body.length > 0 ? Buffer.concat([rewritten, body]) : rewritten;
371979
+ this.openLocal(remote, request2);
371980
+ };
371981
+ const initial = buffered.indexOf(HEADER_END);
371982
+ if (initial !== -1) {
371983
+ flush(initial);
371984
+ return;
371985
+ }
371986
+ const onData = (chunk) => {
371987
+ buffered = Buffer.concat([buffered, chunk]);
371988
+ const end = buffered.indexOf(HEADER_END);
371989
+ if (end !== -1) {
371990
+ remote.off("data", onData);
371991
+ flush(end);
371992
+ } else if (buffered.length > MAX_HEADER_BYTES) {
371993
+ remote.off("data", onData);
371994
+ remote.destroy();
371995
+ }
371996
+ };
371997
+ remote.on("data", onData);
371998
+ }
371999
+ openLocal(remote, request2) {
372000
+ const local = net7.connect({ host: "127.0.0.1", port: this.localPort }, () => {
372001
+ local.write(request2);
371697
372002
  remote.pipe(local);
371698
372003
  local.pipe(remote);
371699
372004
  });
371700
372005
  local.on("error", () => remote.destroy());
371701
- remote.on("close", () => {
371702
- if (!this.closed)
371703
- this.fillPool();
371704
- });
371705
372006
  }
371706
- onIdleClose(remote, errored) {
371707
- if (!this.pool.delete(remote) || this.closed)
371708
- return;
371709
- if (errored) {
371710
- if (this.pool.size === 0)
371711
- this.reconnect();
371712
- } else {
371713
- this.fillPool();
371714
- }
372007
+ rewriteDevOriginHeaders(headers) {
372008
+ const target = `localhost:${this.localPort}`;
372009
+ const text = headers.toString("latin1");
372010
+ let out = text.replace(/^Host:[^\r\n]*\r\n/im, `Host: ${target}\r
372011
+ `);
372012
+ out = out.replace(/^Origin:[^\r\n]*\r\n/im, `Origin: http://${target}\r
372013
+ `);
372014
+ return Buffer.from(out, "latin1");
371715
372015
  }
371716
372016
  async reconnect() {
371717
372017
  this.emit("close");
@@ -371946,21 +372246,33 @@ var DevEnvironment = class extends TypedEventEmitter {
371946
372246
  for (const electric of this.electricInstances) {
371947
372247
  if (electric.pid) {
371948
372248
  try {
371949
- process.kill(electric.pid, "SIGKILL");
372249
+ if (electric.detached) {
372250
+ process.kill(-electric.pid, "SIGKILL");
372251
+ } else {
372252
+ process.kill(electric.pid, "SIGKILL");
372253
+ }
371950
372254
  } catch {
371951
372255
  }
371952
372256
  }
371953
372257
  }
371954
372258
  if (this.drizzleGateway?.pid) {
371955
372259
  try {
371956
- process.kill(this.drizzleGateway.pid, "SIGKILL");
372260
+ if (this.drizzleGateway.detached) {
372261
+ process.kill(-this.drizzleGateway.pid, "SIGKILL");
372262
+ } else {
372263
+ process.kill(this.drizzleGateway.pid, "SIGKILL");
372264
+ }
371957
372265
  } catch {
371958
372266
  }
371959
372267
  }
371960
372268
  for (const resource of this.resources.values()) {
371961
372269
  if (resource.pid) {
371962
372270
  try {
371963
- process.kill(resource.pid, "SIGKILL");
372271
+ if (resource.detached) {
372272
+ process.kill(-resource.pid, "SIGKILL");
372273
+ } else {
372274
+ process.kill(resource.pid, "SIGKILL");
372275
+ }
371964
372276
  } catch {
371965
372277
  }
371966
372278
  }
@@ -372114,21 +372426,33 @@ var DevEnvironment = class extends TypedEventEmitter {
372114
372426
  for (const electric of this.electricInstances) {
372115
372427
  if (electric.pid) {
372116
372428
  try {
372117
- process.kill(electric.pid, "SIGKILL");
372429
+ if (electric.detached) {
372430
+ process.kill(-electric.pid, "SIGKILL");
372431
+ } else {
372432
+ process.kill(electric.pid, "SIGKILL");
372433
+ }
372118
372434
  } catch {
372119
372435
  }
372120
372436
  }
372121
372437
  }
372122
372438
  if (this.drizzleGateway?.pid) {
372123
372439
  try {
372124
- process.kill(this.drizzleGateway.pid, "SIGKILL");
372440
+ if (this.drizzleGateway.detached) {
372441
+ process.kill(-this.drizzleGateway.pid, "SIGKILL");
372442
+ } else {
372443
+ process.kill(this.drizzleGateway.pid, "SIGKILL");
372444
+ }
372125
372445
  } catch {
372126
372446
  }
372127
372447
  }
372128
372448
  for (const resource of this.resources.values()) {
372129
372449
  if (resource.pid) {
372130
372450
  try {
372131
- process.kill(resource.pid, "SIGKILL");
372451
+ if (resource.detached) {
372452
+ process.kill(-resource.pid, "SIGKILL");
372453
+ } else {
372454
+ process.kill(resource.pid, "SIGKILL");
372455
+ }
372132
372456
  } catch {
372133
372457
  }
372134
372458
  }
@@ -372342,6 +372666,13 @@ var DevEnvironment = class extends TypedEventEmitter {
372342
372666
  );
372343
372667
  this.drizzleGateway = drizzleGateway;
372344
372668
  this.systemLog(`Database viewer ready at ${drizzleGateway.url}`);
372669
+ if (drizzleGateway.pid !== void 0) {
372670
+ await stateManager.registerDrizzleGateway({
372671
+ port: drizzleGateway.port,
372672
+ pid: drizzleGateway.pid,
372673
+ detached: drizzleGateway.detached
372674
+ });
372675
+ }
372345
372676
  } catch (err) {
372346
372677
  this.systemLog(
372347
372678
  `Failed to start database viewer: ${err instanceof Error ? err.message : String(err)}`
@@ -373383,7 +373714,7 @@ function trackEvent(event, properties) {
373383
373714
  event,
373384
373715
  properties: {
373385
373716
  ...properties,
373386
- cli_version: "0.1.126",
373717
+ cli_version: "0.1.128",
373387
373718
  platform: process.platform,
373388
373719
  node_version: process.version,
373389
373720
  project_id: getProjectId()
@@ -373701,7 +374032,7 @@ Valid agents: ${VALID_AGENT_IDS.join(", ")}`
373701
374032
  }
373702
374033
 
373703
374034
  // src/commands/docs.tsx
373704
- import { readFileSync as readFileSync9, existsSync as existsSync17 } from "fs";
374035
+ import { readFileSync as readFileSync10, existsSync as existsSync17 } from "fs";
373705
374036
  import { spawn as spawn6 } from "child_process";
373706
374037
  import { join as join19, dirname as dirname8 } from "path";
373707
374038
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -373715,7 +374046,7 @@ var BETA_REGISTRY = [
373715
374046
  ];
373716
374047
 
373717
374048
  // src/lib/beta/storage.ts
373718
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync16, mkdirSync as mkdirSync13 } from "fs";
374049
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync16, mkdirSync as mkdirSync13 } from "fs";
373719
374050
  import { join as join18 } from "path";
373720
374051
  var BETAS_FILE = ".specific/betas.json";
373721
374052
  function loadEnabledBetas(projectDir = process.cwd()) {
@@ -373724,7 +374055,7 @@ function loadEnabledBetas(projectDir = process.cwd()) {
373724
374055
  return [];
373725
374056
  }
373726
374057
  try {
373727
- const content = readFileSync8(filePath, "utf-8");
374058
+ const content = readFileSync9(filePath, "utf-8");
373728
374059
  const data = JSON.parse(content);
373729
374060
  return data.enabled ?? [];
373730
374061
  } catch {
@@ -373829,15 +374160,15 @@ function resolveEmbeddedDoc(path27) {
373829
374160
  function resolveFilesystemDoc(path27) {
373830
374161
  if (!path27) {
373831
374162
  const indexPath2 = join19(docsDir, "index.md");
373832
- return existsSync17(indexPath2) ? readFileSync9(indexPath2, "utf-8") : null;
374163
+ return existsSync17(indexPath2) ? readFileSync10(indexPath2, "utf-8") : null;
373833
374164
  }
373834
374165
  const directPath = join19(docsDir, `${path27}.md`);
373835
374166
  if (existsSync17(directPath)) {
373836
- return readFileSync9(directPath, "utf-8");
374167
+ return readFileSync10(directPath, "utf-8");
373837
374168
  }
373838
374169
  const indexPath = join19(docsDir, path27, "index.md");
373839
374170
  if (existsSync17(indexPath)) {
373840
- return readFileSync9(indexPath, "utf-8");
374171
+ return readFileSync10(indexPath, "utf-8");
373841
374172
  }
373842
374173
  return null;
373843
374174
  }
@@ -374921,11 +375252,13 @@ function DevUI({ instanceKey, tunnelEnabled }) {
374921
375252
  };
374922
375253
  process.on("SIGINT", handleSignal);
374923
375254
  process.on("SIGTERM", handleSignal);
375255
+ process.on("SIGHUP", handleSignal);
374924
375256
  process.on("uncaughtException", handleCrash);
374925
375257
  process.on("unhandledRejection", handleCrash);
374926
375258
  return () => {
374927
375259
  process.off("SIGINT", handleSignal);
374928
375260
  process.off("SIGTERM", handleSignal);
375261
+ process.off("SIGHUP", handleSignal);
374929
375262
  process.off("uncaughtException", handleCrash);
374930
375263
  process.off("unhandledRejection", handleCrash);
374931
375264
  };
@@ -377487,7 +377820,7 @@ function compareVersions(a, b) {
377487
377820
  return 0;
377488
377821
  }
377489
377822
  async function checkForUpdate() {
377490
- const currentVersion = "0.1.126";
377823
+ const currentVersion = "0.1.128";
377491
377824
  const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
377492
377825
  if (!response.ok) {
377493
377826
  throw new Error(`Failed to check for updates: HTTP ${response.status}`);
@@ -377757,7 +378090,7 @@ async function projectListCommand() {
377757
378090
  var program = new Command();
377758
378091
  var env = "production";
377759
378092
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
377760
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.126").enablePositionalOptions();
378093
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.128").enablePositionalOptions();
377761
378094
  program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
377762
378095
  Examples:
377763
378096
  $ specific init