@ouro.bot/cli 0.1.0-alpha.249 → 0.1.0-alpha.250

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/changelog.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.250",
6
+ "changes": [
7
+ "fix(sync): surface 'bundle is not a git repo' as an actionable error instead of silently failing. Previously, enabling `sync.enabled` on a bundle that had never been `git init`'d produced a generic `git status` failure buried in nerves logs; the agent saw nothing in its start-of-turn packet and the user saw nothing in `ouro status`. Now: (1) `preTurnPull` and `postTurnPush` detect the missing `.git` directory before touching git and return an actionable error with the bundle path and the `git init` hint; (2) this error propagates via `ctx.syncFailure` into the agent's Sync warning, so the agent can offer to run `git init` or just do it; (3) `ouro status` shows a red `error` state with `not a git repo — run \\`git init\\` to enable sync` next to the offending bundle. New nerves event: `heart.sync_not_a_repo`."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.249",
6
12
  "changes": [
@@ -87,15 +87,16 @@ function listEnabledBundleAgents(options = {}) {
87
87
  * sync rows without depending on argv-derived global identity. Bundles that
88
88
  * cannot be read are skipped silently.
89
89
  *
90
- * For rows with sync enabled, also resolves the remote URL by shelling out to
91
- * `git remote get-url <remote>` in the bundle directory. On any error (no
92
- * remote, not a git repo, git missing) the URL is left undefined and the
93
- * status renderer treats the bundle as "local only".
90
+ * For rows with sync enabled, also checks whether the bundle is a git repo
91
+ * (via .git directory presence) and resolves the remote URL via
92
+ * `git remote get-url <remote>`. On any error the URL is left undefined and
93
+ * the status renderer falls back to "local only" or "not a git repo".
94
94
  */
95
95
  function listBundleSyncRows(options = {}) {
96
96
  const bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
97
97
  const readFileSync = options.readFileSync ?? fs.readFileSync;
98
98
  const execFileSync = options.execFileSync ?? child_process_1.execFileSync;
99
+ const existsSync = options.existsSync ?? fs.existsSync;
99
100
  const agents = listEnabledBundleAgents(options);
100
101
  const rows = [];
101
102
  for (const agent of agents) {
@@ -117,22 +118,28 @@ function listBundleSyncRows(options = {}) {
117
118
  catch {
118
119
  // Best-effort: bundle without readable config still gets a row with defaults
119
120
  }
120
- let remoteUrl;
121
+ const row = { agent, enabled, remote };
121
122
  if (enabled) {
122
- try {
123
- const out = execFileSync("git", ["remote", "get-url", remote], {
124
- cwd: bundleRoot,
125
- stdio: "pipe",
126
- timeout: 5000,
127
- }).toString().trim();
128
- if (out.length > 0)
129
- remoteUrl = out;
130
- }
131
- catch {
132
- // No remote configured, not a git repo, or git missing — leave undefined.
123
+ // Only meaningful when sync is enabled — we don't care about disabled bundles'
124
+ // git state. Check .git presence before attempting any git invocation.
125
+ const gitInitialized = existsSync(path.join(bundleRoot, ".git"));
126
+ row.gitInitialized = gitInitialized;
127
+ if (gitInitialized) {
128
+ try {
129
+ const out = execFileSync("git", ["remote", "get-url", remote], {
130
+ cwd: bundleRoot,
131
+ stdio: "pipe",
132
+ timeout: 5000,
133
+ }).toString().trim();
134
+ if (out.length > 0)
135
+ row.remoteUrl = out;
136
+ }
137
+ catch {
138
+ // No remote configured or git missing — leave remoteUrl undefined.
139
+ }
133
140
  }
134
141
  }
135
- rows.push(remoteUrl !== undefined ? { agent, enabled, remote, remoteUrl } : { agent, enabled, remote });
142
+ rows.push(row);
136
143
  }
137
144
  return rows;
138
145
  }
@@ -147,10 +147,14 @@ function parseStatusPayload(data) {
147
147
  const remote = stringField(row.remote);
148
148
  if (!agent || enabled === null || !remote)
149
149
  return null;
150
- const remoteUrl = stringField(row.remoteUrl) ?? undefined;
151
- return remoteUrl !== undefined
152
- ? { agent, enabled, remote, remoteUrl }
153
- : { agent, enabled, remote };
150
+ const parsed = { agent, enabled, remote };
151
+ const remoteUrl = stringField(row.remoteUrl);
152
+ if (remoteUrl !== null)
153
+ parsed.remoteUrl = remoteUrl;
154
+ const gitInitialized = booleanField(row.gitInitialized);
155
+ if (gitInitialized !== null)
156
+ parsed.gitInitialized = gitInitialized;
157
+ return parsed;
154
158
  });
155
159
  if (parsedSenses.some((row) => row === null) ||
156
160
  parsedWorkers.some((row) => row === null) ||
@@ -318,11 +322,30 @@ function formatDaemonStatusOutput(response, fallback) {
318
322
  const agentNameWidth = Math.max(12, ...payload.sync.map((r) => r.agent.length));
319
323
  for (const row of payload.sync) {
320
324
  const name = row.agent.padEnd(agentNameWidth);
321
- const dot = row.enabled ? green("●") : dim("○");
322
- const stateText = (row.enabled ? "enabled " : "disabled").padEnd(10);
323
- let detail = "";
324
- if (row.enabled) {
325
- detail = row.remoteUrl !== undefined ? `${row.remote} → ${row.remoteUrl}` : "local only";
325
+ // Three states for enabled rows: error (not a repo), ok (remote URL), ok (local only).
326
+ // Disabled rows show the usual dim dot + "disabled".
327
+ let dot;
328
+ let stateText;
329
+ let detail;
330
+ if (!row.enabled) {
331
+ dot = dim("○");
332
+ stateText = "disabled".padEnd(10);
333
+ detail = "";
334
+ }
335
+ else if (row.gitInitialized === false) {
336
+ dot = red("●");
337
+ stateText = "error ".padEnd(10);
338
+ detail = "not a git repo — run `git init` to enable sync";
339
+ }
340
+ else if (row.remoteUrl !== undefined) {
341
+ dot = green("●");
342
+ stateText = "enabled ".padEnd(10);
343
+ detail = `${row.remote} → ${row.remoteUrl}`;
344
+ }
345
+ else {
346
+ dot = green("●");
347
+ stateText = "enabled ".padEnd(10);
348
+ detail = "local only";
326
349
  }
327
350
  lines.push(` ${name} ${dot} ${stateText} ${dim(detail)}`);
328
351
  }
@@ -39,6 +39,21 @@ const child_process_1 = require("child_process");
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const runtime_1 = require("../nerves/runtime");
42
+ /**
43
+ * Check whether the bundle is initialized as a git repo.
44
+ * Used by both pre-turn pull and post-turn push to surface a clear,
45
+ * actionable error when sync is enabled but the user never ran `git init`
46
+ * inside their bundle. This error is propagated all the way into the
47
+ * agent's start-of-turn packet as a Sync warning, so the agent can
48
+ * either ask the user or run `git init` itself.
49
+ */
50
+ function ensureGitRepo(agentRoot) {
51
+ if (fs.existsSync(path.join(agentRoot, ".git"))) {
52
+ return { ok: true };
53
+ }
54
+ const error = `bundle is not a git repo; run \`git init\` inside ${agentRoot} to enable sync (or disable sync in agent.json)`;
55
+ return { ok: false, error };
56
+ }
42
57
  /**
43
58
  * Pre-turn pull: sync the agent bundle from remote before assembling the start-of-turn packet.
44
59
  *
@@ -54,6 +69,20 @@ function preTurnPull(agentRoot, config) {
54
69
  message: "pre-turn pull starting",
55
70
  meta: { agentRoot, remote: config.remote },
56
71
  });
72
+ // Check that the bundle is actually a git repo before touching git at all.
73
+ // Surfaces a clear, actionable error via syncFailure → start-of-turn packet
74
+ // so the agent can propose running `git init` (or just do it).
75
+ const repoCheck = ensureGitRepo(agentRoot);
76
+ if (!repoCheck.ok) {
77
+ (0, runtime_1.emitNervesEvent)({
78
+ level: "warn",
79
+ component: "heart",
80
+ event: "heart.sync_not_a_repo",
81
+ message: "pre-turn pull failed: bundle is not a git repo",
82
+ meta: { agentRoot },
83
+ });
84
+ return repoCheck;
85
+ }
57
86
  // Check if any remote is configured. If not, skip the pull (local-only mode).
58
87
  try {
59
88
  const remoteOutput = (0, child_process_1.execFileSync)("git", ["remote"], {
@@ -118,6 +147,22 @@ function postTurnPush(agentRoot, config) {
118
147
  message: "post-turn push starting",
119
148
  meta: { agentRoot, remote: config.remote },
120
149
  });
150
+ // Same git-repo check as preTurnPull. This is the more common failure path
151
+ // since postTurnPush runs after every turn while preTurnPull only runs on
152
+ // user-initiated turns. Prior to this guard, an un-init'd bundle would fail
153
+ // the git-status invocation below with a generic "not a git repository"
154
+ // error; now we catch it explicitly with an actionable message.
155
+ const repoCheck = ensureGitRepo(agentRoot);
156
+ if (!repoCheck.ok) {
157
+ (0, runtime_1.emitNervesEvent)({
158
+ level: "warn",
159
+ component: "heart",
160
+ event: "heart.sync_not_a_repo",
161
+ message: "post-turn push failed: bundle is not a git repo",
162
+ meta: { agentRoot },
163
+ });
164
+ return repoCheck;
165
+ }
121
166
  let statusOutput;
122
167
  try {
123
168
  statusOutput = (0, child_process_1.execFileSync)("git", ["status", "--porcelain"], {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.249",
3
+ "version": "0.1.0-alpha.250",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",