@phren/cli 0.0.22 → 0.0.24

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.
@@ -290,9 +290,10 @@ export function repairPreexistingInstall(phrenPath) {
290
290
  function isExpectedVerifyFailure(phrenPath, check) {
291
291
  if (check.ok)
292
292
  return false;
293
- if (check.name === "git-remote")
294
- return true;
295
293
  const prefs = readInstallPreferences(phrenPath);
294
+ // git-remote failure is only expected when the user chose local-only (no clone URL)
295
+ if (check.name === "git-remote")
296
+ return prefs.syncIntent !== "sync";
296
297
  if (check.name === "mcp-config" && prefs.mcpEnabled === false)
297
298
  return true;
298
299
  if (check.name === "hooks-registered" && prefs.hooksEnabled === false)
@@ -339,17 +340,30 @@ function gitRemoteStatus(phrenPath) {
339
340
  catch {
340
341
  return { ok: false, detail: "phren path is not a git repository" };
341
342
  }
343
+ let remote;
342
344
  try {
343
- const remote = execFileSync("git", ["-C", phrenPath, "remote", "get-url", "origin"], {
345
+ remote = execFileSync("git", ["-C", phrenPath, "remote", "get-url", "origin"], {
344
346
  encoding: "utf8",
345
347
  stdio: ["ignore", "pipe", "ignore"],
346
348
  timeout: EXEC_TIMEOUT_QUICK_MS,
347
349
  }).trim();
348
- return remote ? { ok: true, detail: `origin=${remote}` } : { ok: false, detail: "git origin remote not configured" };
350
+ if (!remote)
351
+ return { ok: false, detail: "git origin remote not configured" };
349
352
  }
350
353
  catch {
351
354
  return { ok: false, detail: "git origin remote not configured" };
352
355
  }
356
+ // Connectivity test: verify the remote is reachable (10s timeout)
357
+ try {
358
+ execFileSync("git", ["-C", phrenPath, "ls-remote", "--exit-code", "origin"], {
359
+ stdio: ["ignore", "ignore", "ignore"],
360
+ timeout: 10_000,
361
+ });
362
+ return { ok: true, detail: `origin=${remote}` };
363
+ }
364
+ catch {
365
+ return { ok: false, detail: `origin=${remote} (configured but unreachable)` };
366
+ }
353
367
  }
354
368
  function copyStarterFile(phrenPath, src, dest) {
355
369
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -1147,14 +1161,22 @@ export function runPostInitVerify(phrenPath) {
1147
1161
  }
1148
1162
  else {
1149
1163
  const gitRemote = gitRemoteStatus(phrenPath);
1164
+ const wantSync = prefs.syncIntent === "sync";
1150
1165
  const gitRemoteDetail = gitRemote.ok
1151
1166
  ? gitRemote.detail
1152
- : `${gitRemote.detail} (optional unless you want cross-machine sync)`;
1167
+ : wantSync
1168
+ ? `${gitRemote.detail} — sync was configured but remote is missing or unreachable`
1169
+ : `${gitRemote.detail} (optional unless you want cross-machine sync)`;
1170
+ const gitRemoteFix = gitRemote.ok
1171
+ ? undefined
1172
+ : wantSync
1173
+ ? `Your clone URL didn't work. Fix: cd ${phrenPath} && git remote add origin <URL> && git push -u origin main`
1174
+ : "Optional: initialize a repo and add an origin remote for cross-machine sync.";
1153
1175
  checks.push({
1154
1176
  name: "git-remote",
1155
1177
  ok: gitRemote.ok,
1156
1178
  detail: gitRemoteDetail,
1157
- fix: gitRemote.ok ? undefined : "Optional: initialize a repo and add an origin remote for cross-machine sync.",
1179
+ fix: gitRemoteFix,
1158
1180
  });
1159
1181
  const settingsPath = hookConfigPath("claude");
1160
1182
  const configWritable = nearestWritableTarget(settingsPath);
package/mcp/dist/init.js CHANGED
@@ -104,9 +104,18 @@ function parseRiskySectionsAnswer(raw, fallback) {
104
104
  return Array.from(new Set(parsed));
105
105
  }
106
106
  function hasInstallMarkers(phrenPath) {
107
- return fs.existsSync(phrenPath) && (fs.existsSync(path.join(phrenPath, "machines.yaml")) ||
108
- fs.existsSync(path.join(phrenPath, ".config")) ||
109
- fs.existsSync(path.join(phrenPath, "global")));
107
+ // Require at least two markers to consider this a real install.
108
+ // A partial clone or failed init may create one directory but not finish.
109
+ if (!fs.existsSync(phrenPath))
110
+ return false;
111
+ let found = 0;
112
+ if (fs.existsSync(path.join(phrenPath, "machines.yaml")))
113
+ found++;
114
+ if (fs.existsSync(path.join(phrenPath, ".config")))
115
+ found++;
116
+ if (fs.existsSync(path.join(phrenPath, "global")))
117
+ found++;
118
+ return found >= 2;
110
119
  }
111
120
  function resolveInitPhrenPath(opts) {
112
121
  const raw = opts._walkthroughStoragePath || (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
@@ -1028,9 +1037,23 @@ export async function runInit(opts = {}) {
1028
1037
  }
1029
1038
  catch (e) {
1030
1039
  log(` Clone failed: ${e instanceof Error ? e.message : String(e)}`);
1031
- log(` Continuing with fresh install instead.`);
1040
+ log("");
1041
+ log(" ┌──────────────────────────────────────────────────────────────────┐");
1042
+ log(" │ WARNING: Sync is NOT configured. Your phren data is local-only. │");
1043
+ log(" │ │");
1044
+ log(" │ To fix later: │");
1045
+ log(` │ cd ${phrenPath}`);
1046
+ log(" │ git remote add origin <YOUR_REPO_URL> │");
1047
+ log(" │ git push -u origin main │");
1048
+ log(" └──────────────────────────────────────────────────────────────────┘");
1049
+ log("");
1050
+ log(` Continuing with fresh local-only install.`);
1032
1051
  }
1033
1052
  }
1053
+ // Record sync intent: "sync" if a clone URL was provided (regardless of success), "local" otherwise.
1054
+ // On re-runs of existing installs, preserve the existing syncIntent unless the user provided a new clone URL.
1055
+ const existingSyncIntent = hasExistingInstall ? readInstallPreferences(phrenPath).syncIntent : undefined;
1056
+ const syncIntent = opts._walkthroughCloneUrl ? "sync" : (existingSyncIntent ?? "local");
1034
1057
  const mcpEnabled = opts.mcp ? opts.mcp === "on" : getMcpEnabledPreference(phrenPath);
1035
1058
  const hooksEnabled = opts.hooks ? opts.hooks === "on" : getHooksEnabledPreference(phrenPath);
1036
1059
  const skillsScope = opts.skillsScope ?? "global";
@@ -1185,7 +1208,7 @@ export async function runInit(opts = {}) {
1185
1208
  log(` No starter template updates were applied (starter files not found).`);
1186
1209
  }
1187
1210
  }
1188
- writeInstallPreferences(phrenPath, { mcpEnabled, hooksEnabled, skillsScope, installedVersion: VERSION });
1211
+ writeInstallPreferences(phrenPath, { mcpEnabled, hooksEnabled, skillsScope, installedVersion: VERSION, syncIntent });
1189
1212
  if (repaired.removedLegacyProjects > 0) {
1190
1213
  log(` Removed ${repaired.removedLegacyProjects} legacy starter project entr${repaired.removedLegacyProjects === 1 ? "y" : "ies"} from profiles.`);
1191
1214
  }
@@ -1363,7 +1386,7 @@ export async function runInit(opts = {}) {
1363
1386
  // Configure MCP for all detected AI coding tools and hooks
1364
1387
  configureMcpTargets(phrenPath, { mcpEnabled, hooksEnabled }, "Configured");
1365
1388
  configureHooksIfEnabled(phrenPath, hooksEnabled, "Configured");
1366
- writeInstallPreferences(phrenPath, { mcpEnabled, hooksEnabled, skillsScope, installedVersion: VERSION });
1389
+ writeInstallPreferences(phrenPath, { mcpEnabled, hooksEnabled, skillsScope, installedVersion: VERSION, syncIntent });
1367
1390
  // Post-init verification
1368
1391
  log(`\nVerifying setup...`);
1369
1392
  const verify = runPostInitVerify(phrenPath);
@@ -163,15 +163,52 @@ export function register(server, ctx) {
163
163
  if ((process.env.PHREN_DEBUG))
164
164
  process.stderr.write(`[phren] healthCheck taskMode: ${errorMessage(err)}\n`);
165
165
  }
166
+ let syncIntent;
166
167
  try {
167
168
  const { readInstallPreferences } = await import("./init-preferences.js");
168
169
  const prefs = readInstallPreferences(phrenPath);
169
170
  proactivity = prefs.proactivity || "high";
171
+ syncIntent = prefs.syncIntent;
170
172
  }
171
173
  catch (err) {
172
174
  if ((process.env.PHREN_DEBUG))
173
175
  process.stderr.write(`[phren] healthCheck proactivity: ${errorMessage(err)}\n`);
174
176
  }
177
+ // Determine sync status from intent + git remote state
178
+ let syncStatus = "local-only";
179
+ let syncDetail = "no git remote configured";
180
+ try {
181
+ const { execFileSync } = await import("child_process");
182
+ const remote = execFileSync("git", ["-C", phrenPath, "remote", "get-url", "origin"], {
183
+ encoding: "utf8",
184
+ stdio: ["ignore", "pipe", "ignore"],
185
+ timeout: 5_000,
186
+ }).trim();
187
+ if (remote) {
188
+ try {
189
+ execFileSync("git", ["-C", phrenPath, "ls-remote", "--exit-code", "origin"], {
190
+ stdio: ["ignore", "ignore", "ignore"],
191
+ timeout: 10_000,
192
+ });
193
+ syncStatus = "synced";
194
+ syncDetail = `origin=${remote}`;
195
+ }
196
+ catch {
197
+ syncStatus = syncIntent === "sync" ? "broken" : "local-only";
198
+ syncDetail = `origin=${remote} (unreachable)`;
199
+ }
200
+ }
201
+ else if (syncIntent === "sync") {
202
+ syncStatus = "broken";
203
+ syncDetail = "sync was configured but no remote found";
204
+ }
205
+ }
206
+ catch {
207
+ if (syncIntent === "sync") {
208
+ syncStatus = "broken";
209
+ syncDetail = "sync was configured but no remote found";
210
+ }
211
+ }
175
212
  const lines = [
176
213
  `Phren v${version}`,
177
214
  `Profile: ${activeProfile || "(default)"}`,
@@ -182,6 +219,7 @@ export function register(server, ctx) {
182
219
  `Hooks: ${hooksEnabled ? "enabled" : "disabled"}`,
183
220
  `Proactivity: ${proactivity}`,
184
221
  `Task mode: ${taskMode}`,
222
+ `Sync: ${syncStatus}${syncStatus !== "synced" ? ` (${syncDetail})` : ""}`,
185
223
  `Path: ${phrenPath}`,
186
224
  ].filter(Boolean);
187
225
  return mcpResponse({
@@ -197,6 +235,8 @@ export function register(server, ctx) {
197
235
  hooksEnabled,
198
236
  proactivity,
199
237
  taskMode,
238
+ syncStatus,
239
+ syncDetail,
200
240
  phrenPath,
201
241
  },
202
242
  });