akm-cli 0.10.261007-beta → 0.10.261008-beta

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,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.10.261008] - 2026-10-20
10
+
11
+ ### Fixed
12
+
13
+ - **`akm remember`, config saves and other writes work on filesystems that refuse chmod.** On a CIFS/SMB mount
14
+ such as an Azure Files share, chmod fails with `EPERM`, and akm treated that as fatal: every atomic write
15
+ (`akm remember`, `akm setup`, `akm env`, `akm secret`) and every config backup failed. akm now keeps the mode the
16
+ file was created with when the filesystem refuses chmod (`EPERM`, `ENOTSUP`, `EINVAL`); the mount options decide
17
+ it there, so `env/`, `secrets/` and config backups are not guaranteed owner-only on such a share. Any other chmod
18
+ error still fails the write.
19
+
9
20
  ## [0.10.261007] - 2026-10-20
10
21
 
11
22
  ### Added
@@ -165,8 +165,15 @@ export function writeFileAtomic(target, content, mode) {
165
165
  try {
166
166
  fd = fs.openSync(tmp, "wx", fileMode);
167
167
  tempOwned = true;
168
- if (process.platform !== "win32")
169
- fs.fchmodSync(fd, fileMode);
168
+ if (process.platform !== "win32") {
169
+ try {
170
+ fs.fchmodSync(fd, fileMode);
171
+ }
172
+ catch (error) {
173
+ if (!isChmodUnsupported(error))
174
+ throw error;
175
+ }
176
+ }
170
177
  let offset = 0;
171
178
  while (offset < data.byteLength) {
172
179
  const written = fs.writeSync(fd, data, offset, data.byteLength - offset);
@@ -361,6 +368,14 @@ export function hasErrnoCode(error, code) {
361
368
  return false;
362
369
  return error.code === code;
363
370
  }
371
+ /**
372
+ * True when a chmod failed because the filesystem has no settable POSIX modes
373
+ * (CIFS/SMB such as an Azure Files share, some FUSE mounts). The mount options
374
+ * decide the mode there, so callers keep going with whatever mode the file got.
375
+ */
376
+ export function isChmodUnsupported(error) {
377
+ return hasErrnoCode(error, "EPERM") || hasErrnoCode(error, "ENOTSUP") || hasErrnoCode(error, "EINVAL");
378
+ }
364
379
  /**
365
380
  * True when `value` is a RELATIVE path that cannot leave its base directory:
366
381
  * no absolute form (POSIX `/`, Windows `\` or a `C:` drive prefix), no `~`
@@ -15,7 +15,7 @@
15
15
  import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import { sleepSync } from "../../runtime.js";
18
- import { readTextFile, stripJsonComments, writeFileAtomic } from "../common.js";
18
+ import { isChmodUnsupported, readTextFile, stripJsonComments, writeFileAtomic } from "../common.js";
19
19
  import { ConfigError } from "../errors.js";
20
20
  import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "../file-lock.js";
21
21
  import { getCacheDir, getConfigDir } from "../paths.js";
@@ -88,7 +88,7 @@ export function backupExistingConfig(configPath, now = new Date()) {
88
88
  // env.ts/secret.ts convention — so no other local user can traverse in during
89
89
  // the copy→chmod window. chmod again to tighten a dir from an older version.
90
90
  fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
91
- fs.chmodSync(backupDir, 0o700);
91
+ chmodIfSupported(backupDir, 0o700);
92
92
  const timestamp = now.toISOString().replace(/[.:]/g, "-");
93
93
  let sequence = 0;
94
94
  let timestamped;
@@ -109,11 +109,21 @@ export function backupExistingConfig(configPath, now = new Date()) {
109
109
  // 08-F4: a config backup carries the same sensitive fields as the live config
110
110
  // (endpoints, tokens). `copyFileSync` inherits the source's (often 0644) mode,
111
111
  // so tighten the backups to owner-only — mirrors the env-cli 0600 write floor.
112
- fs.chmodSync(timestamped, 0o600);
113
- fs.chmodSync(latest, 0o600);
112
+ chmodIfSupported(timestamped, 0o600);
113
+ chmodIfSupported(latest, 0o600);
114
114
  pruneOldBackups(backupDir);
115
115
  return { timestamped, latest };
116
116
  }
117
+ /** chmod, skipped on a filesystem without POSIX modes (CIFS), where the mount sets them. */
118
+ function chmodIfSupported(target, mode) {
119
+ try {
120
+ fs.chmodSync(target, mode);
121
+ }
122
+ catch (error) {
123
+ if (!isChmodUnsupported(error))
124
+ throw error;
125
+ }
126
+ }
117
127
  function pruneOldBackups(backupDir) {
118
128
  pruneToNewest(backupDir, MAX_CONFIG_BACKUPS, (entry) => entry.isFile() && isTimestampedConfigBackup(entry.name));
119
129
  }
@@ -26201,8 +26201,14 @@ function writeFileAtomic(target, content, mode) {
26201
26201
  try {
26202
26202
  fd = fs2.openSync(tmp, "wx", fileMode);
26203
26203
  tempOwned = true;
26204
- if (process.platform !== "win32")
26205
- fs2.fchmodSync(fd, fileMode);
26204
+ if (process.platform !== "win32") {
26205
+ try {
26206
+ fs2.fchmodSync(fd, fileMode);
26207
+ } catch (error) {
26208
+ if (!isChmodUnsupported(error))
26209
+ throw error;
26210
+ }
26211
+ }
26206
26212
  let offset = 0;
26207
26213
  while (offset < data.byteLength) {
26208
26214
  const written = fs2.writeSync(fd, data, offset, data.byteLength - offset);
@@ -26351,6 +26357,9 @@ function hasErrnoCode(error, code) {
26351
26357
  return false;
26352
26358
  return error.code === code;
26353
26359
  }
26360
+ function isChmodUnsupported(error) {
26361
+ return hasErrnoCode(error, "EPERM") || hasErrnoCode(error, "ENOTSUP") || hasErrnoCode(error, "EINVAL");
26362
+ }
26354
26363
  function isContainedRelativePath(value) {
26355
26364
  if (value === "" || value.startsWith("/") || value.startsWith("\\") || value.startsWith("~"))
26356
26365
  return false;
@@ -39230,7 +39239,7 @@ function backupExistingConfig(configPath, now = new Date) {
39230
39239
  return;
39231
39240
  const backupDir = path20.join(getCacheDir(), "config-backups");
39232
39241
  fs16.mkdirSync(backupDir, { recursive: true, mode: 448 });
39233
- fs16.chmodSync(backupDir, 448);
39242
+ chmodIfSupported(backupDir, 448);
39234
39243
  const timestamp = now.toISOString().replace(/[.:]/g, "-");
39235
39244
  let sequence = 0;
39236
39245
  let timestamped;
@@ -39247,11 +39256,19 @@ function backupExistingConfig(configPath, now = new Date) {
39247
39256
  }
39248
39257
  const latest = path20.join(backupDir, "config.latest.json");
39249
39258
  fs16.copyFileSync(configPath, latest);
39250
- fs16.chmodSync(timestamped, 384);
39251
- fs16.chmodSync(latest, 384);
39259
+ chmodIfSupported(timestamped, 384);
39260
+ chmodIfSupported(latest, 384);
39252
39261
  pruneOldBackups(backupDir);
39253
39262
  return { timestamped, latest };
39254
39263
  }
39264
+ function chmodIfSupported(target, mode) {
39265
+ try {
39266
+ fs16.chmodSync(target, mode);
39267
+ } catch (error2) {
39268
+ if (!isChmodUnsupported(error2))
39269
+ throw error2;
39270
+ }
39271
+ }
39255
39272
  function pruneOldBackups(backupDir) {
39256
39273
  pruneToNewest(backupDir, MAX_CONFIG_BACKUPS, (entry) => entry.isFile() && isTimestampedConfigBackup(entry.name));
39257
39274
  }
@@ -25529,8 +25529,14 @@ function writeFileAtomic(target, content, mode) {
25529
25529
  try {
25530
25530
  fd = fs2.openSync(tmp, "wx", fileMode);
25531
25531
  tempOwned = true;
25532
- if (process.platform !== "win32")
25533
- fs2.fchmodSync(fd, fileMode);
25532
+ if (process.platform !== "win32") {
25533
+ try {
25534
+ fs2.fchmodSync(fd, fileMode);
25535
+ } catch (error) {
25536
+ if (!isChmodUnsupported(error))
25537
+ throw error;
25538
+ }
25539
+ }
25534
25540
  let offset = 0;
25535
25541
  while (offset < data.byteLength) {
25536
25542
  const written = fs2.writeSync(fd, data, offset, data.byteLength - offset);
@@ -25679,6 +25685,9 @@ function hasErrnoCode(error, code) {
25679
25685
  return false;
25680
25686
  return error.code === code;
25681
25687
  }
25688
+ function isChmodUnsupported(error) {
25689
+ return hasErrnoCode(error, "EPERM") || hasErrnoCode(error, "ENOTSUP") || hasErrnoCode(error, "EINVAL");
25690
+ }
25682
25691
  function isContainedRelativePath(value) {
25683
25692
  if (value === "" || value.startsWith("/") || value.startsWith("\\") || value.startsWith("~"))
25684
25693
  return false;
@@ -38558,7 +38567,7 @@ function backupExistingConfig(configPath, now = new Date) {
38558
38567
  return;
38559
38568
  const backupDir = path20.join(getCacheDir(), "config-backups");
38560
38569
  fs16.mkdirSync(backupDir, { recursive: true, mode: 448 });
38561
- fs16.chmodSync(backupDir, 448);
38570
+ chmodIfSupported(backupDir, 448);
38562
38571
  const timestamp = now.toISOString().replace(/[.:]/g, "-");
38563
38572
  let sequence = 0;
38564
38573
  let timestamped;
@@ -38575,11 +38584,19 @@ function backupExistingConfig(configPath, now = new Date) {
38575
38584
  }
38576
38585
  const latest = path20.join(backupDir, "config.latest.json");
38577
38586
  fs16.copyFileSync(configPath, latest);
38578
- fs16.chmodSync(timestamped, 384);
38579
- fs16.chmodSync(latest, 384);
38587
+ chmodIfSupported(timestamped, 384);
38588
+ chmodIfSupported(latest, 384);
38580
38589
  pruneOldBackups(backupDir);
38581
38590
  return { timestamped, latest };
38582
38591
  }
38592
+ function chmodIfSupported(target, mode) {
38593
+ try {
38594
+ fs16.chmodSync(target, mode);
38595
+ } catch (error2) {
38596
+ if (!isChmodUnsupported(error2))
38597
+ throw error2;
38598
+ }
38599
+ }
38583
38600
  function pruneOldBackups(backupDir) {
38584
38601
  pruneToNewest(backupDir, MAX_CONFIG_BACKUPS, (entry) => entry.isFile() && isTimestampedConfigBackup(entry.name));
38585
38602
  }
@@ -2193,7 +2193,8 @@ service's config; reach for `secret` when one value *is* an auth credential.
2193
2193
  Manage `.env`-backed **environment files** — a group of related **configuration**
2194
2194
  for an app or service (URLs, feature flags, and any credentials it needs),
2195
2195
  loaded together. Each `env` asset is an entire `.env` file stored under `env/`
2196
- in your bundle (mode 0600). Values may or may not be sensitive; **akm protects
2196
+ in your bundle (mode 0600; on a filesystem without POSIX permissions, such as a
2197
+ CIFS/Azure Files share, the mount options decide the mode instead). Values may or may not be sensitive; **akm protects
2197
2198
  them all the same** — key *names* are discoverable; values and comment text
2198
2199
  never appear in structured output (comments routinely contain commented-out
2199
2200
  credentials, so they are treated like values). akm does **not** manage
@@ -2353,7 +2354,9 @@ Manage **secrets** — a single sensitive value used on its own for
2353
2354
  service-account JSON. Where an [env](#env) file holds a *group* of related
2354
2355
  configuration and exposes key *names*, a secret is *one* value and its **entire
2355
2356
  file is the value**, so only the secret's *name* is ever surfaced. Each secret
2356
- is a mode-0600 file under `secrets/` in your bundle.
2357
+ is a mode-0600 file under `secrets/` in your bundle (on a filesystem without
2358
+ POSIX permissions, such as a CIFS/Azure Files share, the mount options decide
2359
+ the mode instead).
2357
2360
 
2358
2361
  This mirrors Docker's secret model (one value per file, mounted at
2359
2362
  `/run/secrets/<name>`, read at runtime, never baked into the image or env at
@@ -97,7 +97,7 @@ Everything in the cache is regenerable. It is safe to delete the entire cache di
97
97
 
98
98
  | Path | Contents | Safe to delete? |
99
99
  |---|---|---|
100
- | `config-backups/config-<timestamp>.json` | Pre-save config snapshots (5 retained; owner-only permissions — file `0600`, dir `0700`, since 08-F4) | Yes |
100
+ | `config-backups/config-<timestamp>.json` | Pre-save config snapshots (5 retained; owner-only permissions — file `0600`, dir `0700`, since 08-F4; on a filesystem without POSIX permissions, such as a CIFS/Azure Files share, the mount options decide them instead) | Yes |
101
101
  | `config-backups/config.latest.json` | Latest backup alias (owner-only `0600`) | Yes |
102
102
  | `registry/` | Downloaded registry tarballs (bundle packages from npm, GitHub, etc.) | Yes — re-downloaded on next `akm bundle add` or `akm bundle update` |
103
103
  | `registry-index/` | Legacy per-URL JSON cache (v0.7 artifact) | Yes — fully replaced by `index.db` in 0.8.0 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.10.261007-beta",
3
+ "version": "0.10.261008-beta",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
6
6
  "keywords": [