Ver Fonte

fix: preserve clonedeps partial sync state

alvinreal há 2 meses atrás
pai
commit
48ea7321ff

+ 6 - 2
docs/clonedeps-feature-plan.md

@@ -43,6 +43,10 @@ Expected flow:
 5. Orchestrator runs the bundled script to sync selected dependencies.
 6. Script writes state, updates ignore files, and performs safe clone/update
    operations.
+7. Orchestrator adds or updates a concise `## Cloned Dependency Source` section
+   in root `AGENTS.md` pointing future agents to `.slim/clonedeps.json` and
+   `.slim/clonedeps/repos/`, with wording that the clones are local cache and
+   may not exist in every checkout.
 
 ## Agent Ownership
 
@@ -318,8 +322,8 @@ Likely tests:
 2. Should the script support sparse checkout for monorepos in MVP, or defer?
 3. Should cloned repos be under `.slim/clonedeps/repos` or a shorter root like
    `.deps-src` for easier manual browsing?
-4. Should the skill update `AGENTS.md` to mention available cloned dependency
-   source, or is `.ignore` visibility enough?
+4. Should `clean` also remove the `AGENTS.md` section, or leave historical
+   guidance for agents? MVP leaves it unless the user asks to remove it.
 
 ## Implementation Sequence
 

+ 4 - 0
docs/skills.md

@@ -74,6 +74,10 @@ The skill is assigned to `orchestrator`. The orchestrator may ask `@librarian`
 to identify important dependencies and resolve official repository URLs/tags,
 then asks for approval before running the bundled sync script.
 
+After syncing, the orchestrator also adds or updates a concise
+`## Cloned Dependency Source` section in root `AGENTS.md`, pointing future agents
+to `.slim/clonedeps.json` and `.slim/clonedeps/repos/`.
+
 Safety defaults:
 
 - direct, important dependencies only;

+ 3 - 0
src/skills/clonedeps/README.md

@@ -18,3 +18,6 @@ node ~/.config/opencode/skills/clonedeps/scripts/clonedeps.mjs clean --root .
 ```
 
 Cloned repositories live under `.slim/clonedeps/repos/` and are ignored by git.
+After syncing, the orchestrator should add or update a concise
+`## Cloned Dependency Source` section in the repo root `AGENTS.md` pointing
+future agents to `.slim/clonedeps.json` and `.slim/clonedeps/repos/`.

+ 27 - 1
src/skills/clonedeps/SKILL.md

@@ -84,7 +84,33 @@ marker blocks, and shallow-clones pinned dependency repositories into:
 .slim/clonedeps/repos/
 ```
 
-### Step 5: Check Status or Clean Up
+### Step 5: Register Dependency Source in AGENTS.md
+
+After a successful sync, update the repository's root `AGENTS.md` so future
+agents know why the dependency source exists and where to look.
+
+If `AGENTS.md` already has a `## Cloned Dependency Source` section, update that
+section. Otherwise append this section:
+
+```markdown
+## Cloned Dependency Source
+
+Selected dependency source repositories are available under
+`.slim/clonedeps/repos/` for local inspection. These clones are ignored by git
+but intentionally unignored for OpenCode visibility. They are local cache and
+may not exist in every checkout.
+
+If `.slim/clonedeps.json` exists, read it before using the clones; it records
+package names, versions/refs, local paths, and why each dependency was cloned.
+
+Use these clones for dependency internals/source inspection. For ordinary API
+usage or current docs, prefer `@librarian`.
+```
+
+Keep the section concise. Do not paste the full clone plan into `AGENTS.md`;
+the detailed source of truth is `.slim/clonedeps.json`.
+
+### Step 6: Check Status or Clean Up
 
 ```bash
 node ~/.config/opencode/skills/clonedeps/scripts/clonedeps.mjs status --root .

+ 4 - 1
src/skills/clonedeps/codemap.md

@@ -25,7 +25,10 @@ updates ignore marker blocks, and shallow-clones selected repositories.
 3. User approves the plan.
 4. Orchestrator runs `sync --plan`, which validates input, updates ignore files,
    verifies refs where possible, clones to temp directories, then writes state.
-5. `status` reports current state; `clean` removes managed clones/state and
+5. Orchestrator updates root `AGENTS.md` with a concise
+   `## Cloned Dependency Source` pointer to `.slim/clonedeps.json` and the clone
+   directory.
+6. `status` reports current state; `clean` removes managed clones/state and
    marker blocks.
 
 ## Integration

+ 38 - 14
src/skills/clonedeps/scripts/clonedeps.mjs

@@ -357,26 +357,50 @@ export function cloneDependency(root, dependency) {
 }
 
 export function sync(root, plan) {
+  return syncWithOperations(root, plan);
+}
+
+export function syncWithOperations(root, plan, operations = {}) {
+  const ops = {
+    cloneDependency,
+    saveState,
+    updateIgnoreFiles,
+    verifyRemoteRef,
+    ...operations,
+  };
   const validated = validatePlan(plan);
-  for (const dependency of validated.dependencies) {
-    clonePathForDependency(root, dependency);
+  const dependencies = [];
+
+  if (validated.dependencies.length === 0) {
+    return ops.saveState(root, dependencies);
   }
 
-  updateIgnoreFiles(root);
-  const dependencies = [];
+  ops.updateIgnoreFiles(root);
 
-  for (const dependency of validated.dependencies) {
-    const refStatus = verifyRemoteRef(dependency.repoUrl, dependency.ref);
-    const result = cloneDependency(root, dependency);
-    dependencies.push({
-      ...dependency,
-      path: normalizePath(path.relative(root, result.path)),
-      refStatus,
-      status: result.status,
-    });
+  try {
+    for (const dependency of validated.dependencies) {
+      const refStatus = ops.verifyRemoteRef(dependency.repoUrl, dependency.ref);
+      const result = ops.cloneDependency(root, dependency);
+      dependencies.push({
+        ...dependency,
+        path: normalizePath(path.relative(root, result.path)),
+        refStatus,
+        status: result.status,
+      });
+      ops.saveState(root, dependencies);
+    }
+  } catch (error) {
+    if (dependencies.length > 0) {
+      try {
+        ops.saveState(root, dependencies);
+      } catch {
+        // Preserve the original clone/fetch error for the caller.
+      }
+    }
+    throw error;
   }
 
-  return saveState(root, dependencies);
+  return ops.saveState(root, dependencies);
 }
 
 export function clean(root) {

+ 63 - 0
src/skills/clonedeps/scripts/clonedeps.test.ts

@@ -19,6 +19,7 @@ const {
   safePackagePathName,
   scanProject,
   saveState,
+  syncWithOperations,
   updateIgnoreFiles,
   validatePlan,
   ValidationError,
@@ -178,6 +179,68 @@ describe('state and ignore file management', () => {
       'oh-my-opencode-slim clonedeps',
     );
   });
+
+  test('sync saves partial state when a later clone fails', () => {
+    const root = createTempDir();
+    const plan = {
+      version: '1.0.0',
+      dependencies: [
+        validPlan().dependencies[0],
+        {
+          ...validPlan({ name: 'zod' }).dependencies[0],
+          resolvedVersion: '4.0.0',
+          repoUrl: 'https://github.com/colinhacks/zod.git',
+          ref: 'v4.0.0',
+        },
+      ],
+    };
+    let clones = 0;
+
+    expect(() =>
+      syncWithOperations(root, plan, {
+        cloneDependency: (_root, dependency) => {
+          clones += 1;
+          if (clones === 2) throw new Error('clone failed');
+          const clonePath = clonePathForDependency(root, dependency);
+          mkdirSync(clonePath, { recursive: true });
+          return { path: clonePath, status: 'cloned' };
+        },
+        saveState,
+        updateIgnoreFiles,
+        verifyRemoteRef: () => 'exact',
+      }),
+    ).toThrow('clone failed');
+
+    expect(loadState(root)?.dependencies).toHaveLength(1);
+    expect(readFileSync(path.join(root, '.gitignore'), 'utf8')).toContain(
+      '.slim/clonedeps/repos/',
+    );
+  });
+
+  test('sync preserves existing state when first clone fails', () => {
+    const root = createTempDir();
+    const dependency = validatePlan(validPlan()).dependencies[0];
+    saveState(root, [
+      {
+        ...dependency,
+        path: '.slim/clonedeps/repos/npm/@opencode-ai__sdk/1.3.17',
+        status: 'cloned',
+      },
+    ]);
+
+    expect(() =>
+      syncWithOperations(root, validPlan(), {
+        cloneDependency: () => {
+          throw new Error('first clone failed');
+        },
+        saveState,
+        updateIgnoreFiles,
+        verifyRemoteRef: () => 'exact',
+      }),
+    ).toThrow('first clone failed');
+
+    expect(loadState(root)?.dependencies).toHaveLength(1);
+  });
 });
 
 describe('scanProject', () => {