Browse Source

fix: preserve clonedeps partial sync state

alvinreal 2 months ago
parent
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.
 5. Orchestrator runs the bundled script to sync selected dependencies.
 6. Script writes state, updates ignore files, and performs safe clone/update
 6. Script writes state, updates ignore files, and performs safe clone/update
    operations.
    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
 ## Agent Ownership
 
 
@@ -318,8 +322,8 @@ Likely tests:
 2. Should the script support sparse checkout for monorepos in MVP, or defer?
 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
 3. Should cloned repos be under `.slim/clonedeps/repos` or a shorter root like
    `.deps-src` for easier manual browsing?
    `.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
 ## 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,
 to identify important dependencies and resolve official repository URLs/tags,
 then asks for approval before running the bundled sync script.
 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:
 Safety defaults:
 
 
 - direct, important dependencies only;
 - 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.
 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/
 .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
 ```bash
 node ~/.config/opencode/skills/clonedeps/scripts/clonedeps.mjs status --root .
 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.
 3. User approves the plan.
 4. Orchestrator runs `sync --plan`, which validates input, updates ignore files,
 4. Orchestrator runs `sync --plan`, which validates input, updates ignore files,
    verifies refs where possible, clones to temp directories, then writes state.
    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.
    marker blocks.
 
 
 ## Integration
 ## Integration

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

@@ -357,26 +357,50 @@ export function cloneDependency(root, dependency) {
 }
 }
 
 
 export function sync(root, plan) {
 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);
   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) {
 export function clean(root) {

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

@@ -19,6 +19,7 @@ const {
   safePackagePathName,
   safePackagePathName,
   scanProject,
   scanProject,
   saveState,
   saveState,
+  syncWithOperations,
   updateIgnoreFiles,
   updateIgnoreFiles,
   validatePlan,
   validatePlan,
   ValidationError,
   ValidationError,
@@ -178,6 +179,68 @@ describe('state and ignore file management', () => {
       'oh-my-opencode-slim clonedeps',
       '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', () => {
 describe('scanProject', () => {