akm-cli 0.9.0-beta.39 → 0.9.0-beta.40

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 CHANGED
@@ -6,6 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Changed
10
+
11
+ - **BEHAVIOR CHANGE — `akm init --dir <path>` no longer silently repoints your
12
+ default stash.** Previously, `akm init --dir X` unconditionally wrote
13
+ `stashDir: X` to `config.json` whenever `X` differed from the configured
14
+ default — so initializing a throwaway or secondary stash (e.g.
15
+ `akm init --dir /tmp/scratch`) would hijack the user's real default stash
16
+ pointer (the footgun documented in `memory:akm-init-persists-stashdir-warning`).
17
+ Now `init` persists `stashDir` to config **only** when one of the following
18
+ holds: (a) **no `--dir`** was provided (the default `~/akm` setup flow —
19
+ unchanged), (b) `--dir` was provided and **no `stashDir` exists in config yet**
20
+ (first-time bootstrap), or (c) `--dir` was provided **with the new
21
+ `--set-default` flag** (explicit opt-in). Otherwise `init` still scaffolds and
22
+ backfills the target dir exactly as before, but **leaves your default stash
23
+ pointer untouched** and prints:
24
+ `Your default stash is unchanged (<existing>). Re-run with --set-default to make <dir> the default.`
25
+ The `InitResponse` JSON gains `defaultStashUpdated: boolean` and an optional
26
+ `previousStashDir`. To make a `--dir` target your default, pass
27
+ `akm init --dir <path> --set-default`. (`akm setup` is unaffected — it remains
28
+ the explicit configuration flow and always sets the default.)
29
+
9
30
  ### Added
10
31
 
11
32
  - **Per-type SOFT authoring conventions are now user-editable stash facts.** A
@@ -53,6 +53,8 @@ function assertInitSandbox(stashDir, dirExplicitlyProvided) {
53
53
  throw new ConfigError(`refusing to persist --dir stashDir to a temporary path while under test runner; set AKM_FORCE_INIT_TMP_STASH=1 if you really mean it (stashDir=${stashDir})`, "INIT_TMP_STASH_REFUSED");
54
54
  }
55
55
  export async function akmInit(options) {
56
+ const dirExplicitlyProvided = options?.dir != null;
57
+ const setDefault = options?.setDefault === true;
56
58
  const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
57
59
  // Safety check (#473): refuse stashDir at /, $HOME, /etc, ~/.config, etc.
58
60
  // Runs BEFORE any disk write — a fat-fingered `akm init --dir /` or
@@ -61,7 +63,7 @@ export async function akmInit(options) {
61
63
  assertSafeStashDir(stashDir);
62
64
  // Defense-in-depth: refuse to persist an explicit --dir /tmp/... stashDir
63
65
  // to config under a test runner. Default HOME-resolved paths are exempt.
64
- assertInitSandbox(stashDir, options?.dir != null);
66
+ assertInitSandbox(stashDir, dirExplicitlyProvided);
65
67
  let created = false;
66
68
  if (!fs.existsSync(stashDir)) {
67
69
  fs.mkdirSync(stashDir, { recursive: true });
@@ -82,11 +84,33 @@ export async function akmInit(options) {
82
84
  // helpers are absent-only: they never overwrite a file a user has edited.
83
85
  copyStashSkeleton(stashDir);
84
86
  scaffoldStashMeta(stashDir);
85
- // Persist stashDir in config.json
87
+ // Persist stashDir in config.json — but ONLY when the user is actually
88
+ // setting up / opting into a default. A bare `akm init --dir <secondary>`
89
+ // must NOT silently repoint the user's real default stash (the footgun
90
+ // documented in memory:akm-init-persists-stashdir-warning).
91
+ //
92
+ // Decision matrix — persist when ANY of:
93
+ // (a) no --dir provided → default HOME-resolved setup flow
94
+ // (b) --dir AND no existing stashDir in config → first-time bootstrap
95
+ // (c) --dir AND --set-default → explicit opt-in
96
+ // Otherwise (--dir + existing default + no --set-default) leave the default
97
+ // pointer alone; the target dir is still scaffolded above.
86
98
  const configPath = getConfigPath();
87
99
  const existing = loadUserConfig();
88
- if (!existing.stashDir || existing.stashDir !== stashDir) {
89
- saveConfig({ ...existing, stashDir });
100
+ const existingStashDir = existing.stashDir;
101
+ const shouldPersist = !dirExplicitlyProvided || !existingStashDir || setDefault;
102
+ let defaultStashUpdated = false;
103
+ let previousStashDir;
104
+ if (shouldPersist) {
105
+ if (!existingStashDir || existingStashDir !== stashDir) {
106
+ saveConfig({ ...existing, stashDir });
107
+ defaultStashUpdated = true;
108
+ }
109
+ // else: already pointed here — no-op, no spurious rewrite.
110
+ }
111
+ else {
112
+ // Default left untouched; surface it so the CLI can inform the user.
113
+ previousStashDir = existingStashDir;
90
114
  }
91
115
  // Ensure ripgrep is available (install to cache/bin if needed)
92
116
  let ripgrep;
@@ -98,7 +122,7 @@ export async function akmInit(options) {
98
122
  catch {
99
123
  // Non-fatal: ripgrep is optional, search works without it
100
124
  }
101
- return { stashDir, created, configPath, ripgrep };
125
+ return { stashDir, created, configPath, defaultStashUpdated, previousStashDir, ripgrep };
102
126
  }
103
127
  /** Initialise `dir` as a git repository if it is not already one. */
104
128
  function ensureGitRepo(dir) {
@@ -48,12 +48,20 @@ export const initCommand = defineJsonCommand({
48
48
  },
49
49
  args: {
50
50
  dir: { type: "string", description: "Custom stash directory path (default: ~/akm)" },
51
+ "set-default": {
52
+ type: "boolean",
53
+ description: "Make --dir the default stash (write stashDir to config.json). Without this, `akm init --dir X` scaffolds X but leaves your existing default stash unchanged.",
54
+ default: false,
55
+ },
51
56
  },
52
57
  async run({ args }) {
53
58
  // Accept both historical spellings for backwards compatibility with
54
59
  // older docs/scripts that used `--stashDir`.
55
60
  const legacyDir = parseFlagValue(process.argv, "--stashDir") ?? parseFlagValue(process.argv, "--stash-dir");
56
- const result = await akmInit({ dir: args.dir ?? legacyDir });
61
+ const result = await akmInit({
62
+ dir: args.dir ?? legacyDir,
63
+ setDefault: getHyphenatedBoolean(args, "set-default"),
64
+ });
57
65
  output("init", result);
58
66
  },
59
67
  });
@@ -1035,8 +1035,14 @@ export function formatCuratePlain(r, detail) {
1035
1035
  }
1036
1036
  export function formatInitPlain(r) {
1037
1037
  let out = `Stash initialized at ${r.stashDir ?? "unknown"}`;
1038
- if (r.configPath)
1038
+ // When --dir scaffolded a secondary stash but the default was deliberately
1039
+ // left untouched, tell the user instead of silently repointing their default.
1040
+ if (r.defaultStashUpdated === false && typeof r.previousStashDir === "string" && r.previousStashDir) {
1041
+ out += `\nYour default stash is unchanged (${r.previousStashDir}). Re-run with --set-default to make ${r.stashDir} the default.`;
1042
+ }
1043
+ else if (r.configPath) {
1039
1044
  out += `\nConfig saved to ${r.configPath}`;
1045
+ }
1040
1046
  return out;
1041
1047
  }
1042
1048
  export function formatIndexPlain(r) {
@@ -1791,7 +1791,7 @@ export async function runSetupWizard(opts) {
1791
1791
  // Bootstrap directory structure before any prompts so the stash exists
1792
1792
  // even if the wizard is interrupted after this point.
1793
1793
  if (!opts?.noInit) {
1794
- await akmInit({ dir: resolvedStashDir });
1794
+ await akmInit({ dir: resolvedStashDir, setDefault: true });
1795
1795
  }
1796
1796
  // Quick connectivity check — skip network-dependent steps when offline
1797
1797
  const online = await isOnline();
@@ -1986,7 +1986,7 @@ export async function runSetupWithDefaults(opts) {
1986
1986
  // Bootstrap directory structure first
1987
1987
  let initResult;
1988
1988
  if (!opts.noInit) {
1989
- initResult = await akmInit({ dir: stashDir });
1989
+ initResult = await akmInit({ dir: stashDir, setDefault: true });
1990
1990
  }
1991
1991
  // Run steps in non-interactive mode (applies defaults, skips prompts)
1992
1992
  const ctx = createSetupContext(current, { nonInteractive: true });
@@ -2283,7 +2283,7 @@ export async function runSetupFromConfig(opts) {
2283
2283
  // Bootstrap directory structure
2284
2284
  let initResult;
2285
2285
  if (!opts.noInit) {
2286
- initResult = await akmInit({ dir: stashDir });
2286
+ initResult = await akmInit({ dir: stashDir, setDefault: true });
2287
2287
  }
2288
2288
  // Optional probe
2289
2289
  const mergedLlm = getDefaultLlmConfig(merged);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.0-beta.39",
3
+ "version": "0.9.0-beta.40",
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": [