akm-cli 0.9.0-beta.40 → 0.9.0-beta.41
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.md +20 -0
- package/dist/sources/providers/git.js +71 -61
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Auto-sync no longer refuses to commit akm's own changes when unrelated
|
|
12
|
+
non-akm files are present in the stash working tree.** When a stash root is
|
|
13
|
+
shared with a project repo, stray files written into the stash root (e.g. a
|
|
14
|
+
`tasks.bak-…` backup dir or report artifacts like `data.js`,
|
|
15
|
+
`akm-health-report.html`, `reports/`) previously tripped the #476 safety
|
|
16
|
+
guard, which threw `refusing to push: … has uncommitted non-akm changes` on
|
|
17
|
+
**every** `akm improve` end-of-run auto-sync, `akm sync`, and `akm push`. In
|
|
18
|
+
one production incident this silently blocked all commits for ~1.5 days while
|
|
19
|
+
akm kept accepting proposals it never persisted. `saveGitStash` now **scopes
|
|
20
|
+
what it stages** instead of refusing: (1) an explicit modified-file list when
|
|
21
|
+
the caller passes `opts.paths`, else (2) the akm-managed pathspecs
|
|
22
|
+
(`TYPE_DIRS` values + `.akm`) that exist on disk — which by construction never
|
|
23
|
+
stages non-akm WIP, preserving the #476 protection without an all-or-nothing
|
|
24
|
+
refusal — and only as a last resort (3) `git add -A` when no managed pathspec
|
|
25
|
+
can be resolved. If nothing akm-managed is staged the run returns
|
|
26
|
+
`nothing to commit` (no empty commit, no throw). Unrelated non-akm files are
|
|
27
|
+
left untouched and uncommitted.
|
|
28
|
+
|
|
9
29
|
### Changed
|
|
10
30
|
|
|
11
31
|
- **BEHAVIOR CHANGE — `akm init --dir <path>` no longer silently repoints your
|
|
@@ -479,28 +479,32 @@ export function saveGitStash(name, message, writableOverride, options) {
|
|
|
479
479
|
if (!statusResult.stdout.trim()) {
|
|
480
480
|
return { committed: false, pushed: false, skipped: false, output: "nothing to commit, working tree clean" };
|
|
481
481
|
}
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
482
|
+
// Scoped staging (#476 + the auto-sync incident): NEVER refuse akm's commit
|
|
483
|
+
// because unrelated non-akm files exist in the working tree. When the stash
|
|
484
|
+
// dir is shared with a non-akm project (stash root == project repo root), a
|
|
485
|
+
// blunt `git add -A` would sweep the user's unrelated WIP into the stash's
|
|
486
|
+
// remote. We avoid that by SCOPING what we stage, not by refusing the commit.
|
|
487
|
+
//
|
|
488
|
+
// Precedence:
|
|
489
|
+
// 1. Explicit modified-file list (`options.paths`) — stage exactly those.
|
|
490
|
+
// 2. Managed pathspecs (TYPE_DIRS values + `.akm`) that exist on disk —
|
|
491
|
+
// stages everything akm owns and, by construction, never stages non-akm
|
|
492
|
+
// WIP. This preserves the #476 protection WITHOUT refusing.
|
|
493
|
+
// 3. Last-resort `git add -A` — ONLY when neither an explicit list nor any
|
|
494
|
+
// managed pathspec can be resolved. This is the maintainer-approved
|
|
495
|
+
// "or all files if we cannot determine the exact file list" fallback and
|
|
496
|
+
// is the one (rare) path that could include unrelated non-akm files.
|
|
497
|
+
if (!stageScopedChanges(repoDir, options?.paths)) {
|
|
498
|
+
throw new Error(`git add failed while staging akm changes in ${repoDir}`);
|
|
499
|
+
}
|
|
500
|
+
// Nothing actually staged → don't create an empty commit. This happens when
|
|
501
|
+
// only non-akm files were dirty (precedence 2 staged nothing).
|
|
502
|
+
const stagedResult = runGit(["-C", repoDir, "diff", "--cached", "--quiet"]);
|
|
503
|
+
if (stagedResult.status === 0) {
|
|
504
|
+
return { committed: false, pushed: false, skipped: false, output: "nothing to commit" };
|
|
505
|
+
}
|
|
506
|
+
// Commit — supply fallback identity so fresh environments without
|
|
498
507
|
// user.name/user.email configured can always commit to the default stash.
|
|
499
|
-
// `add -A` is safe here because nonAkmDirty was just verified empty.
|
|
500
|
-
const addResult = runGit(["-C", repoDir, "add", "-A"]);
|
|
501
|
-
if (addResult.status !== 0) {
|
|
502
|
-
throw new Error(`git add failed: ${addResult.stderr?.trim() || "unknown error"}`);
|
|
503
|
-
}
|
|
504
508
|
const commitResult = runGit([
|
|
505
509
|
"-C",
|
|
506
510
|
repoDir,
|
|
@@ -535,6 +539,51 @@ export function saveGitStash(name, message, writableOverride, options) {
|
|
|
535
539
|
output: (commitResult.stdout + pushResult.stdout).trim() || "changes committed and pushed",
|
|
536
540
|
};
|
|
537
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Stage akm's changes in `repoDir` using the scoped-staging precedence
|
|
544
|
+
* documented at the call site (#476). Returns `false` only when a `git add`
|
|
545
|
+
* subprocess fails; returns `true` otherwise (including when nothing matched —
|
|
546
|
+
* the caller then detects "nothing staged" via `git diff --cached --quiet`).
|
|
547
|
+
*
|
|
548
|
+
* @param paths Optional explicit repo-relative paths akm wrote this run. When
|
|
549
|
+
* provided and non-empty, exactly those are staged (chunked to stay under
|
|
550
|
+
* argv length limits). Otherwise we fall back to the managed pathspecs, and
|
|
551
|
+
* finally to `git add -A` only if no managed pathspec exists on disk.
|
|
552
|
+
*/
|
|
553
|
+
function stageScopedChanges(repoDir, paths) {
|
|
554
|
+
// Precedence 1: explicit modified-file list.
|
|
555
|
+
const explicit = (paths ?? []).filter((p) => typeof p === "string" && p.length > 0);
|
|
556
|
+
if (explicit.length > 0) {
|
|
557
|
+
return addPathspecsChunked(repoDir, explicit);
|
|
558
|
+
}
|
|
559
|
+
// Precedence 2: managed pathspecs that exist on disk (TYPE_DIRS + `.akm`).
|
|
560
|
+
const managed = [...Object.values(TYPE_DIRS), ".akm"].filter((dir) => fs.existsSync(path.join(repoDir, dir)));
|
|
561
|
+
if (managed.length > 0) {
|
|
562
|
+
return addPathspecsChunked(repoDir, managed);
|
|
563
|
+
}
|
|
564
|
+
// Precedence 3 (last resort): nothing akm-managed resolved on disk. Stage
|
|
565
|
+
// everything. This is the ONLY branch that can include unrelated non-akm
|
|
566
|
+
// files — it is the explicit, maintainer-approved "or all files if we cannot
|
|
567
|
+
// determine the exact file list" fallback and should be rare/never in
|
|
568
|
+
// practice (a git-backed stash always has at least one managed subtree once
|
|
569
|
+
// akm has written to it).
|
|
570
|
+
const addAll = runGit(["-C", repoDir, "add", "-A"]);
|
|
571
|
+
return addAll.status === 0;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Run `git add -- <pathspec>...` in chunks so a very large path list never
|
|
575
|
+
* exceeds the OS argv-length limit. Each chunk must succeed.
|
|
576
|
+
*/
|
|
577
|
+
function addPathspecsChunked(repoDir, pathspecs) {
|
|
578
|
+
const CHUNK = 500;
|
|
579
|
+
for (let i = 0; i < pathspecs.length; i += CHUNK) {
|
|
580
|
+
const chunk = pathspecs.slice(i, i + CHUNK);
|
|
581
|
+
const result = runGit(["-C", repoDir, "add", "--", ...chunk]);
|
|
582
|
+
if (result.status !== 0)
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
538
587
|
function findGitStashByTarget(stashes, target) {
|
|
539
588
|
return stashes.find((stash) => matchesGitStashTarget(stash, target));
|
|
540
589
|
}
|
|
@@ -620,45 +669,6 @@ export function classifyCloneFailure(url, stderr, spawnError) {
|
|
|
620
669
|
const detail = raw || spawnMsg || "unknown error";
|
|
621
670
|
return `Failed to clone ${url}: ${detail}`;
|
|
622
671
|
}
|
|
623
|
-
// ── Stash-safety helpers (#476) ──────────────────────────────────────────────
|
|
624
|
-
/**
|
|
625
|
-
* Inspect `git status --porcelain` output and return every dirty path that is
|
|
626
|
-
* NOT inside an akm-managed subtree. Used by `runUpstreamPush` to refuse
|
|
627
|
-
* pushing unrelated WIP when a writable stash shares its root with a project
|
|
628
|
-
* repo.
|
|
629
|
-
*
|
|
630
|
-
* Porcelain v1 format: `XY <path>` or `XY <orig> -> <new>` for renames. We
|
|
631
|
-
* key off the post-rename path (or the only path) — that is the working-tree
|
|
632
|
-
* file at risk of being staged by `git add -A`.
|
|
633
|
-
*/
|
|
634
|
-
function collectNonAkmDirtyPaths(porcelainOutput) {
|
|
635
|
-
const akmDirs = new Set(Object.values(TYPE_DIRS));
|
|
636
|
-
const result = [];
|
|
637
|
-
for (const rawLine of porcelainOutput.split("\n")) {
|
|
638
|
-
const line = rawLine.replace(/\r$/, "");
|
|
639
|
-
if (line.length === 0)
|
|
640
|
-
continue;
|
|
641
|
-
// Skip the 2-char status code + 1 space.
|
|
642
|
-
let p = line.length > 3 ? line.slice(3) : "";
|
|
643
|
-
// Renames / copies: `from -> to`. Stage decision applies to `to`.
|
|
644
|
-
const arrow = p.lastIndexOf(" -> ");
|
|
645
|
-
if (arrow !== -1) {
|
|
646
|
-
p = p.slice(arrow + 4);
|
|
647
|
-
}
|
|
648
|
-
// Strip surrounding quotes for paths with special chars.
|
|
649
|
-
if (p.startsWith('"') && p.endsWith('"') && p.length >= 2) {
|
|
650
|
-
p = p.slice(1, -1);
|
|
651
|
-
}
|
|
652
|
-
if (!p)
|
|
653
|
-
continue;
|
|
654
|
-
const segments = p.split("/");
|
|
655
|
-
const top = segments[0];
|
|
656
|
-
if (top === ".akm" || akmDirs.has(top))
|
|
657
|
-
continue;
|
|
658
|
-
result.push(p);
|
|
659
|
-
}
|
|
660
|
-
return result;
|
|
661
|
-
}
|
|
662
672
|
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
663
|
-
export {
|
|
673
|
+
export { ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
|
|
664
674
|
// resolveWritableOverride is exported at its declaration above.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.41",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|