@uluops/setup 0.6.4 → 0.6.5

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.
@@ -87,7 +87,10 @@ async function readCredentialsFile() {
87
87
  return defaultProfile?.apiKey ?? defaultProfile?.api_key;
88
88
  }
89
89
  async function validateKey(apiKey) {
90
- const url = "https://api.uluops.ai/api/v1/registry/users/me";
90
+ // Self-identity lives in ops-uluops-api (`/api/v1/auth/me`), not the
91
+ // registry-api users namespace. Registry-api `/users/:id` Zod-validates the
92
+ // id as UUID, so /users/me returns 400 with `id: ["Invalid uuid"]`.
93
+ const url = "https://api.uluops.ai/api/v1/auth/me";
91
94
  try {
92
95
  const res = await fetch(url, {
93
96
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -99,8 +102,9 @@ async function validateKey(apiKey) {
99
102
  if (!res.ok) {
100
103
  throw new Error(`API returned ${res.status}. Try --skip-validation to continue offline.`);
101
104
  }
102
- const data = (await res.json());
103
- return { email: data.email ?? null };
105
+ // ops-uluops-api wraps user payloads as { data: { email, ... } }
106
+ const body = (await res.json());
107
+ return { email: body.data?.email ?? null };
104
108
  }
105
109
  catch (err) {
106
110
  // fetch() throws TypeError for network failures (ENOTFOUND, ECONNREFUSED).
@@ -8,3 +8,15 @@ export interface McpResult {
8
8
  export declare function installMcp(profile: HarnessProfile, apiKey: string, scope: "global" | "local", dryRun: boolean): Promise<McpResult>;
9
9
  /** Remove UluOps MCP server entries from the harness config. */
10
10
  export declare function uninstallMcp(profile: HarnessProfile, configPath: string): Promise<void>;
11
+ /**
12
+ * Append `entry` to a .gitignore file, creating it if missing.
13
+ *
14
+ * Discriminates ENOENT (file does not exist → create fresh) from other read
15
+ * errors (permission denied, I/O error, EISDIR → warn and skip). The previous
16
+ * implementation caught everything and wrote a single-line file, silently
17
+ * clobbering user content on any read failure.
18
+ *
19
+ * `reader` is injected for testing the error-discrimination behavior; defaults
20
+ * to the real fs reader.
21
+ */
22
+ export declare function ensureGitignoreEntry(gitignorePath: string, entry: string, reader?: (path: string) => Promise<string>): Promise<void>;
package/dist/steps/mcp.js CHANGED
@@ -38,20 +38,39 @@ async function backupConfig(harnessName, configPath) {
38
38
  }
39
39
  async function addToGitignore(localConfigFilename) {
40
40
  const root = await findProjectRoot();
41
- const gitignorePath = join(root, ".gitignore");
42
41
  try {
43
42
  await access(join(root, ".git"));
44
43
  }
45
44
  catch {
46
45
  return;
47
46
  }
47
+ await ensureGitignoreEntry(join(root, ".gitignore"), localConfigFilename);
48
+ }
49
+ /**
50
+ * Append `entry` to a .gitignore file, creating it if missing.
51
+ *
52
+ * Discriminates ENOENT (file does not exist → create fresh) from other read
53
+ * errors (permission denied, I/O error, EISDIR → warn and skip). The previous
54
+ * implementation caught everything and wrote a single-line file, silently
55
+ * clobbering user content on any read failure.
56
+ *
57
+ * `reader` is injected for testing the error-discrimination behavior; defaults
58
+ * to the real fs reader.
59
+ */
60
+ export async function ensureGitignoreEntry(gitignorePath, entry, reader = (p) => readFile(p, "utf-8")) {
61
+ let content;
48
62
  try {
49
- const content = await readFile(gitignorePath, "utf-8");
50
- if (content.includes(localConfigFilename))
51
- return;
52
- await atomicWrite(gitignorePath, content.trimEnd() + `\n${localConfigFilename}\n`);
63
+ content = await reader(gitignorePath);
53
64
  }
54
- catch {
55
- await atomicWrite(gitignorePath, `${localConfigFilename}\n`);
65
+ catch (err) {
66
+ if (err.code === "ENOENT") {
67
+ await atomicWrite(gitignorePath, `${entry}\n`);
68
+ return;
69
+ }
70
+ console.warn(`Warning: could not read ${gitignorePath} (${err.message}). Skipping .gitignore update.`);
71
+ return;
56
72
  }
73
+ if (content.includes(entry))
74
+ return;
75
+ await atomicWrite(gitignorePath, content.trimEnd() + `\n${entry}\n`);
57
76
  }
@@ -24,12 +24,13 @@ export async function writeShellExport(profilePath, apiKey, dryRun) {
24
24
  return;
25
25
  }
26
26
  const startIdx = content.indexOf(FENCE_START);
27
- const endIdx = content.indexOf(FENCE_END);
27
+ const endIdx = content.lastIndexOf(FENCE_END);
28
28
  if (!dryRun) {
29
29
  await backupProfile(profilePath);
30
30
  }
31
31
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
32
- // Replace existing fenced block (use last FENCE_END after FENCE_START to handle duplicates)
32
+ // Replace existing fenced block using lastIndexOf for FENCE_END collapses
33
+ // any duplicate blocks left by earlier buggy installs into a single new block
33
34
  const before = content.slice(0, startIdx);
34
35
  const after = content.slice(endIdx + FENCE_END.length);
35
36
  if (!dryRun) {
@@ -52,7 +53,7 @@ export async function removeShellExport(profilePath) {
52
53
  return;
53
54
  }
54
55
  const startIdx = content.indexOf(FENCE_START);
55
- const endIdx = content.indexOf(FENCE_END);
56
+ const endIdx = content.lastIndexOf(FENCE_END);
56
57
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
57
58
  await backupProfile(profilePath);
58
59
  const before = content.slice(0, startIdx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uluops/setup",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Zero-friction installer for UluOps agentic harnesses",
5
5
  "license": "MIT",
6
6
  "repository": {