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 +11 -0
- package/dist/core/common.js +17 -2
- package/dist/core/config/config-io.js +14 -4
- package/dist/scripts/akm-migrate-node.js +22 -5
- package/dist/scripts/akm-migrate.js +22 -5
- package/docs/reference/cli.md +5 -2
- package/docs/reference/data-and-telemetry.md +1 -1
- package/package.json +1 -1
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
|
package/dist/core/common.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38579
|
-
|
|
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
|
}
|
package/docs/reference/cli.md
CHANGED
|
@@ -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
|
|
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.
|
|
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": [
|