totopo 0.7.0 → 0.8.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.
package/bin/totopo.js CHANGED
@@ -5,8 +5,8 @@
5
5
  // =============================================================================
6
6
 
7
7
  import { execSync, spawnSync } from "node:child_process";
8
- import { existsSync, openSync, readFileSync, realpathSync } from "node:fs";
9
- import { basename, dirname, join } from "node:path";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { basename, dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
 
12
12
  // ─── Guard: inside container ──────────────────────────────────────────────────
@@ -25,9 +25,8 @@ try {
25
25
  }
26
26
 
27
27
  // ─── Paths ────────────────────────────────────────────────────────────────────
28
- // realpathSync resolves the npm symlink in node_modules/.bin/ back to the
29
- // real package root, so TOTOPO_PACKAGE_DIR is always the installed package.
30
- const packageDir = dirname(dirname(realpathSync(fileURLToPath(import.meta.url))));
28
+ // dirname(dirname(...)) walks up from bin/ to the package root.
29
+ const packageDir = dirname(dirname(fileURLToPath(import.meta.url)));
31
30
 
32
31
  let repoRoot;
33
32
  try {
@@ -44,34 +43,39 @@ try {
44
43
  process.env.TOTOPO_PACKAGE_DIR = packageDir;
45
44
  process.env.TOTOPO_REPO_ROOT = repoRoot;
46
45
 
47
- // ─── Auto-install dependencies ────────────────────────────────────────────────
48
- const tsx = join(packageDir, "node_modules/.bin/tsx");
49
- if (!existsSync(tsx)) {
50
- process.stdout.write(" Getting ready…");
51
- let pm = "npm";
52
- try {
53
- execSync("which pnpm", { stdio: "ignore" });
54
- pm = "pnpm";
55
- } catch {}
56
- execSync(`${pm} install --silent`, { cwd: packageDir, stdio: "inherit" });
57
- process.stdout.write("\r\x1b[2K"); // clear the line
46
+ // ─── Guard: dist/ must exist ─────────────────────────────────────────────────
47
+ if (!existsSync(new URL("../dist/commands/sync-dockerfile.js", import.meta.url))) {
48
+ console.error("");
49
+ console.error(" totopo: compiled output not found.");
50
+ console.error(" This should not happen with a published package.");
51
+ console.error(" If you are developing locally, run: pnpm build");
52
+ console.error("");
53
+ process.exit(1);
58
54
  }
59
55
 
60
- // ─── Helper ───────────────────────────────────────────────────────────────────
61
- const run = (script, args = []) => spawnSync(tsx, [join(packageDir, `src/core/commands/${script}`), ...args], { stdio: "inherit" });
56
+ // ─── Import compiled commands ─────────────────────────────────────────────────
57
+ const { run: syncDockerfile } = await import("../dist/commands/sync-dockerfile.js");
58
+ const { run: doctor } = await import("../dist/commands/doctor.js");
59
+ const { run: onboard } = await import("../dist/commands/onboard.js");
60
+ const { run: menu } = await import("../dist/commands/menu.js");
61
+ const { run: dev } = await import("../dist/commands/dev.js");
62
+ const { run: stop } = await import("../dist/commands/stop.js");
63
+ const { run: rebuild } = await import("../dist/commands/rebuild.js");
64
+ const { run: manage } = await import("../dist/commands/manage.js");
65
+ const { run: settings } = await import("../dist/commands/settings.js");
62
66
 
63
67
  // ─── Onboarding ───────────────────────────────────────────────────────────────
64
- if (!existsSync(join(repoRoot, ".totopo/Dockerfile"))) {
65
- run("onboard.ts");
66
- if (!existsSync(join(repoRoot, ".totopo/Dockerfile"))) process.exit(0);
68
+ if (!existsSync(`${repoRoot}/.totopo/Dockerfile`)) {
69
+ const completed = await onboard(packageDir, repoRoot);
70
+ if (!completed) process.exit(0);
67
71
  }
68
72
 
69
73
  // ─── Sync Dockerfile with host runtimes ───────────────────────────────────────
70
- run("sync-dockerfile.ts");
74
+ await syncDockerfile(packageDir, repoRoot);
71
75
 
72
76
  // ─── Doctor (silent pre-check) ────────────────────────────────────────────────
73
- const doctor = run("doctor.ts");
74
- if (doctor.status !== 0) {
77
+ const doctorResult = await doctor(repoRoot, false);
78
+ if (!doctorResult.ok) {
75
79
  console.error(" Fix the issues above and re-run totopo.");
76
80
  console.error("");
77
81
  process.exit(1);
@@ -85,7 +89,6 @@ const dockerResult = spawnSync("docker", ["ps", "--filter", "name=totopo-managed
85
89
  });
86
90
  const activeCount = dockerResult.stdout ? dockerResult.stdout.trim().split("\n").filter(Boolean).length : 0;
87
91
 
88
- // Is THIS project's container running?
89
92
  const projectContainerResult = spawnSync("docker", ["ps", "--filter", `name=totopo-managed-${projectName}`, "--format", "{{.Names}}"], {
90
93
  encoding: "utf8",
91
94
  });
@@ -95,12 +98,11 @@ const projectRunning = (projectContainerResult.stdout ?? "")
95
98
  .filter(Boolean)
96
99
  .some((n) => n === `totopo-managed-${projectName}`);
97
100
 
98
- // Does THIS project's image exist?
99
101
  const projectImageResult = spawnSync("docker", ["images", "-q", `totopo-managed-${projectName}`], { encoding: "utf8" });
100
102
  const projectImageExists = (projectImageResult.stdout ?? "").trim().length > 0;
101
103
 
102
104
  let hasKey = false;
103
- const envPath = join(repoRoot, ".totopo/.env");
105
+ const envPath = `${repoRoot}/.totopo/.env`;
104
106
  if (existsSync(envPath)) {
105
107
  for (const line of readFileSync(envPath, "utf8").split("\n")) {
106
108
  const trimmed = line.trim();
@@ -113,55 +115,35 @@ if (existsSync(envPath)) {
113
115
  }
114
116
  }
115
117
 
116
- // ─── Interactive menu (clack) ─────────────────────────────────────────────────
117
- // stdout → /dev/tty so the clack UI renders on the terminal
118
- // stderr → pipe so the selected action string is captured
119
- const ttyFd = openSync("/dev/tty", "w");
120
-
118
+ // ─── Interactive menu loop ────────────────────────────────────────────────────
121
119
  let showMenu = true;
122
120
  while (showMenu) {
123
121
  showMenu = false;
124
122
 
125
- const menuResult = spawnSync(
126
- tsx,
127
- [
128
- join(packageDir, "src/core/commands/menu.ts"),
129
- projectName,
130
- String(activeCount),
131
- String(hasKey),
132
- String(projectRunning),
133
- String(projectImageExists),
134
- ],
135
- {
136
- stdio: ["inherit", ttyFd, "pipe"],
137
- encoding: "utf8",
138
- },
139
- );
140
- const action = (menuResult.stderr ?? "").trim();
141
-
142
- // ─── Execute selection ────────────────────────────────────────────────────────
123
+ const action = await menu({ projectName, activeCount, hasKey, projectRunning, projectImageExists });
124
+
143
125
  switch (action) {
144
126
  case "dev":
145
- run("dev.ts");
127
+ await dev(packageDir, repoRoot);
146
128
  break;
147
129
  case "stop":
148
- run("stop.ts", [projectName]);
130
+ await stop(projectName);
149
131
  break;
150
132
  case "rebuild":
151
- run("rebuild.ts", [projectName]);
152
- run("dev.ts");
133
+ await rebuild(projectName);
134
+ await dev(packageDir, repoRoot);
153
135
  break;
154
136
  case "manage": {
155
- const result = run("manage.ts", [projectName]);
156
- if (result.status === 2) showMenu = true; // Back selected
137
+ const result = await manage(projectName, repoRoot);
138
+ if (result === "back") showMenu = true;
157
139
  break;
158
140
  }
159
141
  case "doctor":
160
- run("doctor.ts", ["--verbose"]);
142
+ await doctor(repoRoot, true);
161
143
  break;
162
144
  case "settings": {
163
- const result = run("settings.ts");
164
- if (result.status === 2) showMenu = true; // Back selected
145
+ const result = await settings(packageDir, repoRoot);
146
+ if (result === "back") showMenu = true;
165
147
  break;
166
148
  }
167
149
  default: