@anyforge/control 0.2.2 → 0.2.3

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.
@@ -1,4 +1,5 @@
1
1
  import { promises as fs } from 'fs';
2
+ import { execFileSync } from 'child_process';
2
3
  import { homedir } from 'os';
3
4
  import { join } from 'path';
4
5
  import open from 'open';
@@ -14,8 +15,18 @@ export async function runInit(opts) {
14
15
  console.log(bold('AnyForge Control — device authorisation'));
15
16
  // eslint-disable-next-line no-console
16
17
  console.log(dim(` endpoint: ${endpoint}`));
17
- // 1. Issue a one-time code.
18
- const initRes = await fetch(`${endpoint}/api/cli/init`, { method: 'POST' });
18
+ // 1. Issue a one-time code. We detect the git remote up-front when
19
+ // running in a repo so it can be attached to the API key later.
20
+ // Detection happens before scope-pick so the server has it
21
+ // regardless of which scope the operator chooses.
22
+ const cwd0 = process.cwd();
23
+ const gitRoot0 = findGitRoot(cwd0);
24
+ const earlyRepo = gitRoot0 ? detectGitRemoteRepo(gitRoot0) : null;
25
+ const initRes = await fetch(`${endpoint}/api/cli/init`, {
26
+ method: 'POST',
27
+ headers: { 'content-type': 'application/json' },
28
+ body: JSON.stringify({ repo: earlyRepo }),
29
+ });
19
30
  const init = (await initRes.json());
20
31
  if (!init.ok || !init.data) {
21
32
  throw new Error(`Could not start the authorisation flow: ${init.error ?? 'unknown error'}`);
@@ -50,7 +61,7 @@ export async function runInit(opts) {
50
61
  // contract and avoids accidentally writing settings into a
51
62
  // subdirectory the operator didn't mean to target.
52
63
  const cwd = process.cwd();
53
- const repoRoot = (await isGitRepoRoot(cwd)) ? cwd : null;
64
+ const repoRoot = findGitRoot(cwd);
54
65
  const scope = await pickScope(repoRoot);
55
66
  // 6. Patch the chosen target. apiKey is inlined into the env map
56
67
  // so Claude Code can read it without a shell restart.
@@ -146,15 +157,17 @@ async function hasClaudeCodeLogin() {
146
157
  return false;
147
158
  }
148
159
  }
149
- async function isGitRepoRoot(cwd) {
150
- // git worktrees use a `.git` file pointing at the real gitdir, so
151
- // accept either a directory or a regular file.
160
+ function findGitRoot(cwd) {
161
+ // Works from any subdirectory — git walks up the tree to find the root.
152
162
  try {
153
- const stat = await fs.stat(join(cwd, '.git'));
154
- return stat.isDirectory() || stat.isFile();
163
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
164
+ cwd,
165
+ encoding: 'utf8',
166
+ stdio: ['pipe', 'pipe', 'pipe'],
167
+ }).trim();
155
168
  }
156
169
  catch {
157
- return false;
170
+ return null;
158
171
  }
159
172
  }
160
173
  async function pickScope(repoRoot) {
@@ -186,6 +199,36 @@ function summarisePatches(patches) {
186
199
  console.log(` ${dim('•')} ${p.label} ${dim('(' + p.path + ')')}`);
187
200
  }
188
201
  }
202
+ /**
203
+ * Read the `origin` remote URL and normalise it to `owner/repo`.
204
+ * Handles both SSH (`git@github.com:owner/repo.git`) and HTTPS forms.
205
+ * Returns null when there is no remote, git is unavailable, or the
206
+ * URL can't be parsed into an owner/repo pair.
207
+ *
208
+ * Uses execFileSync (not exec/execSync) so no shell interpolation
209
+ * occurs — the args array is passed directly to the git binary.
210
+ */
211
+ function detectGitRemoteRepo(repoRoot) {
212
+ try {
213
+ const raw = execFileSync('git', ['remote', 'get-url', 'origin'], {
214
+ cwd: repoRoot,
215
+ encoding: 'utf8',
216
+ stdio: ['pipe', 'pipe', 'pipe'],
217
+ }).trim();
218
+ // SSH: git@github.com:owner/repo.git
219
+ const sshMatch = raw.match(/^git@[\w.-]+:([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
220
+ if (sshMatch)
221
+ return sshMatch[1];
222
+ // HTTPS: https://github.com/owner/repo[.git]
223
+ const httpsMatch = raw.match(/^https?:\/\/[\w.-]+\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
224
+ if (httpsMatch)
225
+ return httpsMatch[1];
226
+ return null;
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ }
189
232
  // Re-exported so future commands (e.g. status) can reuse the prompt
190
233
  // shape without duplicating the question text.
191
234
  export { prompts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anyforge/control",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "AnyForge Control CLI — governed LLM proxy for Claude Code, Cursor, Cline, and any agent that talks to OpenAI- or Anthropic-shape APIs.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://anyforge.ai",