@specific.dev/cli 0.1.106 → 0.1.108

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 +194 -82
  64. package/package.json +5 -2
  65. /package/dist/admin/_next/static/{P3VZVUKKu1Qjrf-CPDzDC → -uB2tvCTW06hUMOZfRURZ}/_buildManifest.js +0 -0
  66. /package/dist/admin/_next/static/{P3VZVUKKu1Qjrf-CPDzDC → -uB2tvCTW06hUMOZfRURZ}/_clientMiddlewareManifest.json +0 -0
  67. /package/dist/admin/_next/static/{P3VZVUKKu1Qjrf-CPDzDC → -uB2tvCTW06hUMOZfRURZ}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -602,12 +602,12 @@ var init_is_wsl = __esm({
602
602
  // node_modules/.pnpm/powershell-utils@0.1.0/node_modules/powershell-utils/index.js
603
603
  import process3 from "node:process";
604
604
  import { Buffer as Buffer2 } from "node:buffer";
605
- import { promisify } from "node:util";
605
+ import { promisify as promisify2 } from "node:util";
606
606
  import childProcess from "node:child_process";
607
- var execFile, powerShellPath, executePowerShell;
607
+ var execFile2, powerShellPath, executePowerShell;
608
608
  var init_powershell_utils = __esm({
609
609
  "node_modules/.pnpm/powershell-utils@0.1.0/node_modules/powershell-utils/index.js"() {
610
- execFile = promisify(childProcess.execFile);
610
+ execFile2 = promisify2(childProcess.execFile);
611
611
  powerShellPath = () => `${process3.env.SYSTEMROOT || process3.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
612
612
  executePowerShell = async (command, options2 = {}) => {
613
613
  const {
@@ -615,7 +615,7 @@ var init_powershell_utils = __esm({
615
615
  ...execFileOptions
616
616
  } = options2;
617
617
  const encodedCommand = executePowerShell.encodeCommand(command);
618
- return execFile(
618
+ return execFile2(
619
619
  psPath ?? powerShellPath(),
620
620
  [
621
621
  ...executePowerShell.argumentsPrefix,
@@ -658,17 +658,17 @@ var init_utilities = __esm({
658
658
  });
659
659
 
660
660
  // node_modules/.pnpm/wsl-utils@0.3.1/node_modules/wsl-utils/index.js
661
- import { promisify as promisify2 } from "node:util";
661
+ import { promisify as promisify3 } from "node:util";
662
662
  import childProcess2 from "node:child_process";
663
663
  import fs17, { constants as fsConstants } from "node:fs/promises";
664
- var execFile2, wslDrivesMountPoint, powerShellPathFromWsl, powerShellPath2, canAccessPowerShellPromise, canAccessPowerShell, wslDefaultBrowser, convertWslPathToWindows;
664
+ var execFile3, wslDrivesMountPoint, powerShellPathFromWsl, powerShellPath2, canAccessPowerShellPromise, canAccessPowerShell, wslDefaultBrowser, convertWslPathToWindows;
665
665
  var init_wsl_utils = __esm({
666
666
  "node_modules/.pnpm/wsl-utils@0.3.1/node_modules/wsl-utils/index.js"() {
667
667
  init_is_wsl();
668
668
  init_powershell_utils();
669
669
  init_utilities();
670
670
  init_is_wsl();
671
- execFile2 = promisify2(childProcess2.execFile);
671
+ execFile3 = promisify3(childProcess2.execFile);
672
672
  wslDrivesMountPoint = /* @__PURE__ */ (() => {
673
673
  const defaultMountPoint = "/mnt/";
674
674
  let mountPoint;
@@ -724,7 +724,7 @@ var init_wsl_utils = __esm({
724
724
  return path27;
725
725
  }
726
726
  try {
727
- const { stdout } = await execFile2("wslpath", ["-aw", path27], { encoding: "utf8" });
727
+ const { stdout } = await execFile3("wslpath", ["-aw", path27], { encoding: "utf8" });
728
728
  return stdout.trim();
729
729
  } catch {
730
730
  return path27;
@@ -756,14 +756,14 @@ var init_define_lazy_prop = __esm({
756
756
  });
757
757
 
758
758
  // node_modules/.pnpm/default-browser-id@5.0.1/node_modules/default-browser-id/index.js
759
- import { promisify as promisify3 } from "node:util";
759
+ import { promisify as promisify4 } from "node:util";
760
760
  import process4 from "node:process";
761
- import { execFile as execFile3 } from "node:child_process";
761
+ import { execFile as execFile4 } from "node:child_process";
762
762
  async function defaultBrowserId() {
763
763
  if (process4.platform !== "darwin") {
764
764
  throw new Error("macOS only");
765
765
  }
766
- const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
766
+ const { stdout } = await execFileAsync2("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
767
767
  const match = /LSHandlerRoleAll = "(?!-)(?<id>[^"]+?)";\s+?LSHandlerURLScheme = (?:http|https);/.exec(stdout);
768
768
  const browserId = match?.groups.id ?? "com.apple.Safari";
769
769
  if (browserId === "com.apple.safari") {
@@ -771,17 +771,17 @@ async function defaultBrowserId() {
771
771
  }
772
772
  return browserId;
773
773
  }
774
- var execFileAsync;
774
+ var execFileAsync2;
775
775
  var init_default_browser_id = __esm({
776
776
  "node_modules/.pnpm/default-browser-id@5.0.1/node_modules/default-browser-id/index.js"() {
777
- execFileAsync = promisify3(execFile3);
777
+ execFileAsync2 = promisify4(execFile4);
778
778
  }
779
779
  });
780
780
 
781
781
  // node_modules/.pnpm/run-applescript@7.1.0/node_modules/run-applescript/index.js
782
782
  import process5 from "node:process";
783
- import { promisify as promisify4 } from "node:util";
784
- import { execFile as execFile4, execFileSync } from "node:child_process";
783
+ import { promisify as promisify5 } from "node:util";
784
+ import { execFile as execFile5, execFileSync } from "node:child_process";
785
785
  async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
786
786
  if (process5.platform !== "darwin") {
787
787
  throw new Error("macOS only");
@@ -791,13 +791,13 @@ async function runAppleScript(script, { humanReadableOutput = true, signal } = {
791
791
  if (signal) {
792
792
  execOptions.signal = signal;
793
793
  }
794
- const { stdout } = await execFileAsync2("osascript", ["-e", script, outputArguments], execOptions);
794
+ const { stdout } = await execFileAsync3("osascript", ["-e", script, outputArguments], execOptions);
795
795
  return stdout.trim();
796
796
  }
797
- var execFileAsync2;
797
+ var execFileAsync3;
798
798
  var init_run_applescript = __esm({
799
799
  "node_modules/.pnpm/run-applescript@7.1.0/node_modules/run-applescript/index.js"() {
800
- execFileAsync2 = promisify4(execFile4);
800
+ execFileAsync3 = promisify5(execFile5);
801
801
  }
802
802
  });
803
803
 
@@ -813,9 +813,9 @@ var init_bundle_name = __esm({
813
813
  });
814
814
 
815
815
  // node_modules/.pnpm/default-browser@5.4.0/node_modules/default-browser/windows.js
816
- import { promisify as promisify5 } from "node:util";
817
- import { execFile as execFile5 } from "node:child_process";
818
- async function defaultBrowser(_execFileAsync = execFileAsync3) {
816
+ import { promisify as promisify6 } from "node:util";
817
+ import { execFile as execFile6 } from "node:child_process";
818
+ async function defaultBrowser(_execFileAsync = execFileAsync4) {
819
819
  const { stdout } = await _execFileAsync("reg", [
820
820
  "QUERY",
821
821
  " HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
@@ -833,10 +833,10 @@ async function defaultBrowser(_execFileAsync = execFileAsync3) {
833
833
  }
834
834
  return browser;
835
835
  }
836
- var execFileAsync3, windowsBrowserProgIds, _windowsBrowserProgIdMap, UnknownBrowserError;
836
+ var execFileAsync4, windowsBrowserProgIds, _windowsBrowserProgIdMap, UnknownBrowserError;
837
837
  var init_windows = __esm({
838
838
  "node_modules/.pnpm/default-browser@5.4.0/node_modules/default-browser/windows.js"() {
839
- execFileAsync3 = promisify5(execFile5);
839
+ execFileAsync4 = promisify6(execFile6);
840
840
  windowsBrowserProgIds = {
841
841
  MSEdgeHTM: { name: "Edge", id: "com.microsoft.edge" },
842
842
  // The missing `L` is correct.
@@ -863,9 +863,9 @@ var init_windows = __esm({
863
863
  });
864
864
 
865
865
  // node_modules/.pnpm/default-browser@5.4.0/node_modules/default-browser/index.js
866
- import { promisify as promisify6 } from "node:util";
866
+ import { promisify as promisify7 } from "node:util";
867
867
  import process6 from "node:process";
868
- import { execFile as execFile6 } from "node:child_process";
868
+ import { execFile as execFile7 } from "node:child_process";
869
869
  async function defaultBrowser2() {
870
870
  if (process6.platform === "darwin") {
871
871
  const id = await defaultBrowserId();
@@ -873,7 +873,7 @@ async function defaultBrowser2() {
873
873
  return { name, id };
874
874
  }
875
875
  if (process6.platform === "linux") {
876
- const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
876
+ const { stdout } = await execFileAsync5("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
877
877
  const id = stdout.trim();
878
878
  const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
879
879
  return { name, id };
@@ -883,14 +883,14 @@ async function defaultBrowser2() {
883
883
  }
884
884
  throw new Error("Only macOS, Linux, and Windows are supported");
885
885
  }
886
- var execFileAsync4, titleize;
886
+ var execFileAsync5, titleize;
887
887
  var init_default_browser = __esm({
888
888
  "node_modules/.pnpm/default-browser@5.4.0/node_modules/default-browser/index.js"() {
889
889
  init_default_browser_id();
890
890
  init_bundle_name();
891
891
  init_windows();
892
892
  init_windows();
893
- execFileAsync4 = promisify6(execFile6);
893
+ execFileAsync5 = promisify7(execFile7);
894
894
  titleize = (string) => string.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
895
895
  }
896
896
  });
@@ -185191,6 +185191,8 @@ import { readFile, writeFile } from "fs/promises";
185191
185191
  import { existsSync as existsSync5 } from "fs";
185192
185192
  import * as fs6 from "fs";
185193
185193
  import * as path6 from "path";
185194
+ import { execFile } from "child_process";
185195
+ import { promisify } from "util";
185194
185196
  import * as http from "http";
185195
185197
  import * as fs7 from "fs";
185196
185198
  import * as path7 from "path";
@@ -369936,6 +369938,7 @@ function startService(service, resources, secrets, configs, endpointPorts, servi
369936
369938
  }
369937
369939
  };
369938
369940
  }
369941
+ var execFileAsync = promisify(execFile);
369939
369942
  var InstanceStateManager = class {
369940
369943
  stateDir;
369941
369944
  statePath;
@@ -370036,6 +370039,7 @@ var InstanceStateManager = class {
370036
370039
  const content = fs6.readFileSync(this.statePath, "utf-8");
370037
370040
  const state = JSON.parse(content);
370038
370041
  if (!this.isProcessRunning(state.owner.pid)) {
370042
+ await this.killOrphanedProcesses(state);
370039
370043
  fs6.unlinkSync(this.statePath);
370040
370044
  return true;
370041
370045
  }
@@ -370051,6 +370055,35 @@ var InstanceStateManager = class {
370051
370055
  releaseLock();
370052
370056
  }
370053
370057
  }
370058
+ /**
370059
+ * Kill orphaned child processes left behind by a dead owner.
370060
+ * Best-effort: failures are silently ignored.
370061
+ */
370062
+ async killOrphanedProcesses(state) {
370063
+ for (const service of Object.values(state.services)) {
370064
+ if (service.pid && this.isProcessRunning(service.pid)) {
370065
+ try {
370066
+ process.kill(-service.pid, "SIGKILL");
370067
+ } catch {
370068
+ }
370069
+ }
370070
+ }
370071
+ for (const db of Object.values(state.databases)) {
370072
+ if (db.syncUrl) {
370073
+ try {
370074
+ const url = new URL(db.syncUrl);
370075
+ const port = parseInt(url.port, 10);
370076
+ if (!isNaN(port)) {
370077
+ const pid = await findPidOnPort(port);
370078
+ if (pid !== void 0) {
370079
+ process.kill(pid, "SIGKILL");
370080
+ }
370081
+ }
370082
+ } catch {
370083
+ }
370084
+ }
370085
+ }
370086
+ }
370054
370087
  async claimOwnership(command) {
370055
370088
  const releaseLock = await this.acquireLock();
370056
370089
  try {
@@ -370143,6 +370176,19 @@ var InstanceStateManager = class {
370143
370176
  fs6.renameSync(tmpPath, this.statePath);
370144
370177
  }
370145
370178
  };
370179
+ async function findPidOnPort(port) {
370180
+ try {
370181
+ const { stdout } = await execFileAsync(
370182
+ "lsof",
370183
+ ["-i", `:${port}`, "-t", "-sTCP:LISTEN"],
370184
+ { timeout: 5e3 }
370185
+ );
370186
+ const pid = parseInt(stdout.trim().split("\n")[0], 10);
370187
+ return isNaN(pid) ? void 0 : pid;
370188
+ } catch {
370189
+ return void 0;
370190
+ }
370191
+ }
370146
370192
  var __dirname = path7.dirname(fileURLToPath(import.meta.url));
370147
370193
  var adminDir = path7.join(__dirname, "admin");
370148
370194
  var _embeddedAdmin = null;
@@ -370399,6 +370445,22 @@ async function startElectric(postgres, port, dataDir, options2) {
370399
370445
  );
370400
370446
  const secret = generateRandomString(32);
370401
370447
  const host = "127.0.0.1";
370448
+ if (await checkTcpPort2(host, port)) {
370449
+ let freed = false;
370450
+ for (let i = 0; i < 30; i++) {
370451
+ await sleep2(100);
370452
+ if (!await checkTcpPort2(host, port)) {
370453
+ freed = true;
370454
+ break;
370455
+ }
370456
+ }
370457
+ if (!freed) {
370458
+ throw new Error(
370459
+ `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.`
370460
+ );
370461
+ }
370462
+ writeLog("electric", `Port ${port} was occupied but is now free`);
370463
+ }
370402
370464
  const storageDir = path9.join(process.cwd(), dataDir, `electric-${postgres.name}`);
370403
370465
  fs8.rmSync(storageDir, { recursive: true, force: true });
370404
370466
  writeLog("electric", `Starting Electric for database "${postgres.name}"`);
@@ -370423,6 +370485,7 @@ async function startElectric(postgres, port, dataDir, options2) {
370423
370485
  await waitForTcpPort2(host, port);
370424
370486
  return {
370425
370487
  databaseName: postgres.name,
370488
+ pid: electric.pid,
370426
370489
  port,
370427
370490
  url: `http://${host}:${port}`,
370428
370491
  secret,
@@ -370793,6 +370856,29 @@ function runReshape(args, databaseUrl, migrationsDir, reshapeBinaryPath, log) {
370793
370856
  return { success: false, output: message };
370794
370857
  }
370795
370858
  }
370859
+ function getReshapeStatus(databaseUrl, reshapeBinaryPath, log) {
370860
+ const args = ["status", "--format", "json", "--url", databaseUrl];
370861
+ log(`Executing: reshape ${args.join(" ")}`);
370862
+ try {
370863
+ const result = spawnSync(reshapeBinaryPath, args, {
370864
+ encoding: "utf-8",
370865
+ stdio: ["pipe", "pipe", "pipe"]
370866
+ });
370867
+ const stdout = result.stdout || "";
370868
+ const stderr = result.stderr || "";
370869
+ if (result.status !== 0) {
370870
+ log(`Reshape status command failed with exit code ${result.status}`);
370871
+ if (stderr.trim()) log(` stderr: ${stderr.trim()}`);
370872
+ return null;
370873
+ }
370874
+ log(`Reshape status: ${stdout.trim()}`);
370875
+ return JSON.parse(stdout);
370876
+ } catch (err) {
370877
+ const message = err instanceof Error ? err.message : String(err);
370878
+ log(`Failed to get Reshape status: ${message}`);
370879
+ return null;
370880
+ }
370881
+ }
370796
370882
  function makeReadOnly(filePath, log) {
370797
370883
  try {
370798
370884
  fs10.chmodSync(filePath, 292);
@@ -370818,64 +370904,50 @@ function createReshapeWatcher(options2) {
370818
370904
  return null;
370819
370905
  }
370820
370906
  log(`Processing ${currentMigrationFiles.length} migration file(s)`);
370821
- if (currentMigrationFiles.length === 1) {
370822
- const migrationFile = currentMigrationFiles[0];
370823
- const migrationName = getMigrationName(migrationFile);
370824
- log(`Single migration detected: ${migrationFile}`);
370825
- log(`Starting migration "${migrationName}" (will not be completed, allowing iteration)`);
370826
- const result = runReshape(["start"], databaseUrl, migrationsDir, reshapeBinaryPath, log);
370827
- if (!result.success) {
370828
- log(`ERROR: Failed to start migration "${migrationName}"`);
370829
- onError(new Error(`Failed to start migration: ${result.output}`));
370830
- return null;
370831
- }
370832
- startedMigration = migrationFile;
370833
- log(`Migration "${migrationName}" started successfully`);
370834
- log(` Started migration file: ${startedMigration}`);
370835
- } else {
370907
+ const lastMigration = currentMigrationFiles[currentMigrationFiles.length - 1];
370908
+ const lastMigrationName = getMigrationName(lastMigration);
370909
+ const status = getReshapeStatus(databaseUrl, reshapeBinaryPath, log);
370910
+ if (status?.status === "in_progress") {
370911
+ log(`Found in-progress migration(s), aborting before re-initialization...`);
370912
+ runReshape(["abort"], databaseUrl, migrationsDir, reshapeBinaryPath, log);
370913
+ }
370914
+ if (currentMigrationFiles.length > 1) {
370836
370915
  const secondToLast = currentMigrationFiles[currentMigrationFiles.length - 2];
370837
370916
  const secondToLastName = getMigrationName(secondToLast);
370838
- const lastMigration = currentMigrationFiles[currentMigrationFiles.length - 1];
370839
- const lastMigrationName = getMigrationName(lastMigration);
370840
- log(`Multiple migrations detected (${currentMigrationFiles.length} total)`);
370841
- log(` Migrations to complete: ${currentMigrationFiles.slice(0, -1).join(", ")}`);
370842
- log(` Migration to start (latest): ${lastMigration}`);
370843
- log(`Completing all migrations up to and including "${secondToLastName}"...`);
370844
- const completeResult = runReshape(
370845
- ["start", "--complete", "--migration", secondToLastName],
370846
- databaseUrl,
370847
- migrationsDir,
370848
- reshapeBinaryPath,
370849
- log
370850
- );
370851
- if (!completeResult.success) {
370852
- log(`ERROR: Failed to complete migrations up to "${secondToLastName}"`);
370853
- onError(new Error(`Failed to complete migrations: ${completeResult.output}`));
370854
- return null;
370917
+ if (status?.latest_completed_migration !== secondToLastName) {
370918
+ log(`Completing all migrations up to and including "${secondToLastName}"...`);
370919
+ const completeResult = runReshape(
370920
+ ["start", "--complete", "--migration", secondToLastName],
370921
+ databaseUrl,
370922
+ migrationsDir,
370923
+ reshapeBinaryPath,
370924
+ log
370925
+ );
370926
+ if (!completeResult.success) {
370927
+ log(`ERROR: Failed to complete migrations up to "${secondToLastName}"`);
370928
+ onError(new Error(`Failed to complete migrations: ${completeResult.output}`));
370929
+ return null;
370930
+ }
370931
+ log(`Successfully completed ${currentMigrationFiles.length - 1} migration(s)`);
370932
+ } else {
370933
+ log(`Migrations up to "${secondToLastName}" already completed, skipping`);
370855
370934
  }
370856
- log(`Successfully completed ${currentMigrationFiles.length - 1} migration(s)`);
370857
- log(`Making completed migration files read-only...`);
370858
370935
  for (let i = 0; i < currentMigrationFiles.length - 1; i++) {
370859
370936
  const filePath = path11.join(migrationsDir, currentMigrationFiles[i]);
370860
370937
  makeReadOnly(filePath, log);
370861
370938
  }
370862
- log(`Starting latest migration "${lastMigrationName}" (will not be completed, allowing iteration)`);
370863
- const startResult = runReshape(["start"], databaseUrl, migrationsDir, reshapeBinaryPath, log);
370864
- if (!startResult.success) {
370865
- log(`ERROR: Failed to start migration "${lastMigrationName}"`);
370866
- onError(new Error(`Failed to start last migration: ${startResult.output}`));
370867
- return null;
370868
- }
370869
- startedMigration = lastMigration;
370870
- log(`Migration "${lastMigrationName}" started successfully`);
370871
- log(` Started migration file: ${startedMigration}`);
370872
370939
  }
370873
- const searchPath = startedMigration ? getSchemaName(startedMigration) : null;
370874
- if (searchPath) {
370875
- log(`Search path (schema name): ${searchPath}`);
370876
- } else {
370877
- log(`No search_path (no migration started)`);
370940
+ log(`Starting latest migration "${lastMigrationName}" (will not be completed, allowing iteration)`);
370941
+ const startResult = runReshape(["start"], databaseUrl, migrationsDir, reshapeBinaryPath, log);
370942
+ if (!startResult.success) {
370943
+ log(`ERROR: Failed to start migration "${lastMigrationName}"`);
370944
+ onError(new Error(`Failed to start migration: ${startResult.output}`));
370945
+ return null;
370878
370946
  }
370947
+ startedMigration = lastMigration;
370948
+ log(`Migration "${lastMigrationName}" started successfully`);
370949
+ const searchPath = getSchemaName(startedMigration);
370950
+ log(`Search path (schema name): ${searchPath}`);
370879
370951
  log(`Reshape initialization complete`);
370880
370952
  return searchPath;
370881
370953
  };
@@ -371711,6 +371783,9 @@ var DevEnvironment = class extends TypedEventEmitter {
371711
371783
  collectedSecrets = {};
371712
371784
  collectedConfigs = {};
371713
371785
  inputResolve = null;
371786
+ // Last-resort exit handler to kill child processes when the process exits
371787
+ // unexpectedly (e.g. SIGKILL from an agent). Runs synchronously.
371788
+ exitHandler = null;
371714
371789
  constructor(options2) {
371715
371790
  super();
371716
371791
  this.projectDir = options2?.projectDir ?? process.cwd();
@@ -371913,6 +371988,7 @@ var DevEnvironment = class extends TypedEventEmitter {
371913
371988
  if (this.stateManager) {
371914
371989
  await this.stateManager.releaseOwnership();
371915
371990
  }
371991
+ this.removeExitHandler();
371916
371992
  this.systemLog("Shutdown complete");
371917
371993
  closeDebugLog();
371918
371994
  this.setStatus("idle");
@@ -371922,6 +371998,41 @@ var DevEnvironment = class extends TypedEventEmitter {
371922
371998
  this.startResolve = null;
371923
371999
  }
371924
372000
  }
372001
+ // ── Private: exit handler ─────────────────────────────────────────────────
372002
+ /**
372003
+ * Register a synchronous process 'exit' handler that kills all known child
372004
+ * PIDs. This is a last resort for when the process exits without going
372005
+ * through shutdownInternal (e.g. parent sends SIGKILL).
372006
+ */
372007
+ registerExitHandler() {
372008
+ this.removeExitHandler();
372009
+ this.exitHandler = () => {
372010
+ for (const electric of this.electricInstances) {
372011
+ if (electric.pid) {
372012
+ try {
372013
+ process.kill(electric.pid, "SIGKILL");
372014
+ } catch {
372015
+ }
372016
+ }
372017
+ }
372018
+ for (const service of this.services) {
372019
+ const pid = service.process.pid;
372020
+ if (pid) {
372021
+ try {
372022
+ process.kill(-pid, "SIGKILL");
372023
+ } catch {
372024
+ }
372025
+ }
372026
+ }
372027
+ };
372028
+ process.on("exit", this.exitHandler);
372029
+ }
372030
+ removeExitHandler() {
372031
+ if (this.exitHandler) {
372032
+ process.removeListener("exit", this.exitHandler);
372033
+ this.exitHandler = null;
372034
+ }
372035
+ }
371925
372036
  // ── Private: config file watcher ───────────────────────────────────────────
371926
372037
  startConfigWatcher() {
371927
372038
  const configPath = path14.join(this.projectDir, "specific.hcl");
@@ -372099,6 +372210,7 @@ var DevEnvironment = class extends TypedEventEmitter {
372099
372210
  if (result.cancelled) return;
372100
372211
  resources = result.resources;
372101
372212
  this.mailServers = result.mail;
372213
+ this.registerExitHandler();
372102
372214
  } catch (err) {
372103
372215
  const errorMsg = `Failed to start resources: ${err instanceof Error ? err.message : String(err)}`;
372104
372216
  writeLog("system:error", errorMsg);
@@ -373075,7 +373187,7 @@ function trackEvent(event, properties) {
373075
373187
  event,
373076
373188
  properties: {
373077
373189
  ...properties,
373078
- cli_version: "0.1.106",
373190
+ cli_version: "0.1.108",
373079
373191
  platform: process.platform,
373080
373192
  node_version: process.version,
373081
373193
  project_id: getProjectId()
@@ -373512,7 +373624,7 @@ import { render as render3, Text as Text3, Box as Box3 } from "ink";
373512
373624
  import Spinner2 from "ink-spinner";
373513
373625
  import * as fs21 from "fs";
373514
373626
  import * as path19 from "path";
373515
- import { execFile as execFile7 } from "child_process";
373627
+ import { execFile as execFile8 } from "child_process";
373516
373628
 
373517
373629
  // node_modules/.pnpm/@specific+config@file+..+config/node_modules/@specific/config/dist/parser.js
373518
373630
  var import_hcl2_json_parser3 = __toESM(require_dist2(), 1);
@@ -374166,7 +374278,7 @@ async function runReshapeCheck(migrationsDir) {
374166
374278
  const binary = await ensureBinary(reshapeBinary);
374167
374279
  const reshapePath = binary.executables["reshape"];
374168
374280
  return new Promise((resolve9) => {
374169
- execFile7(reshapePath, ["check", "--dirs", migrationsDir], (err, _stdout, stderr) => {
374281
+ execFile8(reshapePath, ["check", "--dirs", migrationsDir], (err, _stdout, stderr) => {
374170
374282
  if (err) {
374171
374283
  const errorMsg = stderr.trim() || err.message;
374172
374284
  resolve9({ success: false, error: errorMsg });
@@ -376754,7 +376866,7 @@ function compareVersions(a, b) {
376754
376866
  return 0;
376755
376867
  }
376756
376868
  async function checkForUpdate() {
376757
- const currentVersion = "0.1.106";
376869
+ const currentVersion = "0.1.108";
376758
376870
  const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
376759
376871
  if (!response.ok) {
376760
376872
  throw new Error(`Failed to check for updates: HTTP ${response.status}`);
@@ -377022,7 +377134,7 @@ async function projectListCommand() {
377022
377134
  var program = new Command();
377023
377135
  var env = "production";
377024
377136
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
377025
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.106").enablePositionalOptions();
377137
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.108").enablePositionalOptions();
377026
377138
  program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
377027
377139
  Examples:
377028
377140
  $ specific init
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/cli",
3
- "version": "0.1.106",
3
+ "version": "0.1.108",
4
4
  "description": "CLI for Specific infrastructure-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -17,7 +17,10 @@
17
17
  "link:dev": "npm run build:dev && npm link",
18
18
  "link:staging": "npm run build:staging && npm link",
19
19
  "link:prod": "npm run build && npm link",
20
- "build:binary": "npx tsx scripts/build-binary.ts"
20
+ "build:binary": "npx tsx scripts/build-binary.ts",
21
+ "link:binary:dev": "NODE_ENV=development npm run build:binary && codesign -s - dist/specific && cp dist/specific ~/.local/bin/specific",
22
+ "link:binary:staging": "NODE_ENV=staging npm run build:binary && codesign -s - dist/specific && cp dist/specific ~/.local/bin/specific",
23
+ "link:binary:prod": "npm run build:binary && codesign -s - dist/specific && cp dist/specific ~/.local/bin/specific"
21
24
  },
22
25
  "keywords": [
23
26
  "infrastructure",