akm-cli 0.8.0-rc.7 → 0.8.0-rc.9
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 +16 -0
- package/README.md +2 -2
- package/dist/cli.js +314 -53
- package/dist/commands/consolidate.js +173 -19
- package/dist/commands/health.js +11 -19
- package/dist/commands/improve.js +8 -0
- package/dist/commands/secret.js +171 -0
- package/dist/core/asset-registry.js +2 -0
- package/dist/core/asset-spec.js +15 -0
- package/dist/core/common.js +1 -0
- package/dist/core/config-schema.js +5 -0
- package/dist/core/paths.js +14 -60
- package/dist/core/state-db.js +1 -1
- package/dist/core/warn.js +4 -2
- package/dist/indexer/db.js +17 -0
- package/dist/indexer/matchers.js +14 -0
- package/dist/indexer/metadata.js +16 -5
- package/dist/llm/client.js +5 -0
- package/dist/output/renderers.js +32 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes.js +1 -0
- package/dist/registry/factory.js +1 -1
- package/dist/registry/providers/skills-sh.js +1 -1
- package/dist/scripts/migrate-storage.js +42 -8
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +20 -5
- package/dist/sources/providers/git.js +25 -9
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.0] - 2026-05-28
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Consolidation `delete_failed` on stale index entries** — when consolidation
|
|
14
|
+
successfully deleted a memory file, the index DB was not re-indexed between
|
|
15
|
+
runs. Subsequent runs loaded the stale DB entry into their memory map, the LLM
|
|
16
|
+
re-proposed the deletion, and `deleteAssetFromSource` threw "not found in
|
|
17
|
+
source" — appearing as `delete_failed` in skipReasons. Fix: `loadMemoriesForSource`
|
|
18
|
+
now filters entries whose file no longer exists on disk before building chunks,
|
|
19
|
+
so phantom memories are never sent to the LLM. A secondary catch in the delete
|
|
20
|
+
handler emits `delete_already_gone` instead of `delete_failed` when the file
|
|
21
|
+
is confirmed absent.
|
|
22
|
+
|
|
9
23
|
> **CI / Docker users:** the 0.8.0 storage split moved `akm.lock`, the event
|
|
10
24
|
> database, and the registry cache out of `$XDG_CONFIG_HOME/akm/` into
|
|
11
25
|
> `$XDG_DATA_HOME`, `$XDG_STATE_HOME`, and `$XDG_CACHE_HOME` respectively. If
|
|
@@ -65,6 +79,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
65
79
|
|
|
66
80
|
### Changed
|
|
67
81
|
|
|
82
|
+
- **Rebrand**: the full name "Agent Kit Manager" is now **Agent Knowledge Management** — `akm` stands for Agent Knowledge Management going forward. The binary name, npm package (`akm-cli`), and all APIs remain unchanged.
|
|
83
|
+
|
|
68
84
|
- **Config layer rewrite** — single-source-of-truth Zod schema in
|
|
69
85
|
`src/core/config-schema.ts` replaces the per-field parse switch AND
|
|
70
86
|
the per-shape load-time parser. Adding a new config field is now one
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# akm -- Agent
|
|
1
|
+
# akm -- Agent Knowledge Management
|
|
2
2
|
|
|
3
|
-
> **akm** (Agent
|
|
3
|
+
> **akm** (Agent Knowledge Management) -- A package manager for AI agent skills, commands, tools, and knowledge.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/akm-cli)
|
|
6
6
|
[](https://www.npmjs.com/package/akm-cli)
|
package/dist/cli.js
CHANGED
|
@@ -2051,6 +2051,258 @@ const vaultCommand = defineCommand({
|
|
|
2051
2051
|
});
|
|
2052
2052
|
},
|
|
2053
2053
|
});
|
|
2054
|
+
// ── secret ──────────────────────────────────────────────────────────────────
|
|
2055
|
+
//
|
|
2056
|
+
// `akm secret` manages whole-file secrets under each stash's secrets/ directory.
|
|
2057
|
+
// Unlike vaults (.env key/value), the ENTIRE file is the secret value. The bytes
|
|
2058
|
+
// are NEVER written to stdout or structured output. Values reach a command only
|
|
2059
|
+
// via `akm secret run` (injected into a child env var) or `akm secret path`
|
|
2060
|
+
// (the Docker /run/secrets + `_FILE` convention).
|
|
2061
|
+
function parseSecretRef(ref) {
|
|
2062
|
+
return parseAssetRef(ref.includes(":") ? ref : `secret:${ref}`);
|
|
2063
|
+
}
|
|
2064
|
+
function makeSecretRef(name, source) {
|
|
2065
|
+
return source?.registryId ? `${source.registryId}//secret:${name}` : `secret:${name}`;
|
|
2066
|
+
}
|
|
2067
|
+
function resolveSecretPath(ref) {
|
|
2068
|
+
const parsed = parseSecretRef(ref);
|
|
2069
|
+
if (parsed.type !== "secret") {
|
|
2070
|
+
throw new UsageError(`Expected a secret ref (secret:<name>); got "${ref}".`);
|
|
2071
|
+
}
|
|
2072
|
+
// Source resolution is identical for every asset type; reuse the vault helper.
|
|
2073
|
+
const source = findVaultSource(parsed.origin);
|
|
2074
|
+
const typeRoot = path.join(source.path, "secrets");
|
|
2075
|
+
const absPath = resolveAssetPathFromName("secret", typeRoot, parsed.name);
|
|
2076
|
+
// Defense-in-depth: ensure the resolved path stays inside the secrets dir.
|
|
2077
|
+
if (!isWithin(absPath, typeRoot)) {
|
|
2078
|
+
throw new UsageError(`Secret name "${parsed.name}" escapes the secrets directory.`);
|
|
2079
|
+
}
|
|
2080
|
+
return { name: parsed.name, absPath, source };
|
|
2081
|
+
}
|
|
2082
|
+
/** Walk `secrets/` across all stashes, returning one entry per secret file. */
|
|
2083
|
+
function listSecretsRecursive() {
|
|
2084
|
+
const result = [];
|
|
2085
|
+
for (const source of resolveSourceEntries(undefined, loadConfig())) {
|
|
2086
|
+
const secretsDir = path.join(source.path, "secrets");
|
|
2087
|
+
if (!fs.existsSync(secretsDir))
|
|
2088
|
+
continue;
|
|
2089
|
+
const walk = (dir) => {
|
|
2090
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2091
|
+
const full = path.join(dir, entry.name);
|
|
2092
|
+
if (entry.isDirectory()) {
|
|
2093
|
+
walk(full);
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
if (!entry.isFile())
|
|
2097
|
+
continue;
|
|
2098
|
+
if (entry.name.endsWith(".lock") || entry.name.endsWith(".sensitive"))
|
|
2099
|
+
continue;
|
|
2100
|
+
// A sibling `<name>.sensitive` marker suppresses listing.
|
|
2101
|
+
if (fs.existsSync(`${full}.sensitive`))
|
|
2102
|
+
continue;
|
|
2103
|
+
const canonical = deriveCanonicalAssetName("secret", secretsDir, full);
|
|
2104
|
+
if (!canonical)
|
|
2105
|
+
continue;
|
|
2106
|
+
result.push({ ref: makeSecretRef(canonical, source), path: full });
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
walk(secretsDir);
|
|
2110
|
+
}
|
|
2111
|
+
return result;
|
|
2112
|
+
}
|
|
2113
|
+
const secretListCommand = defineCommand({
|
|
2114
|
+
meta: {
|
|
2115
|
+
name: "list",
|
|
2116
|
+
description: "List all secrets across all stashes by name (the file contents are never shown)",
|
|
2117
|
+
},
|
|
2118
|
+
run() {
|
|
2119
|
+
return runWithJsonErrors(async () => {
|
|
2120
|
+
output("secret-list", { secrets: listSecretsRecursive() });
|
|
2121
|
+
});
|
|
2122
|
+
},
|
|
2123
|
+
});
|
|
2124
|
+
const secretSetCommand = defineCommand({
|
|
2125
|
+
meta: {
|
|
2126
|
+
name: "set",
|
|
2127
|
+
description: "Create or overwrite a secret. The value is read from stdin by default (never via argv). Use --from-file <path> to import an existing file byte-exact, or --from-env <VAR> to read from an environment variable. Multi-line values are allowed.",
|
|
2128
|
+
},
|
|
2129
|
+
args: {
|
|
2130
|
+
ref: { type: "positional", description: "Secret ref (e.g. secret:deploy-key or just deploy-key)", required: true },
|
|
2131
|
+
"from-file": { type: "string", description: "Read the value from this file (stored byte-exact)" },
|
|
2132
|
+
"from-env": { type: "string", description: "Read the value from the named environment variable" },
|
|
2133
|
+
},
|
|
2134
|
+
run({ args }) {
|
|
2135
|
+
return runWithJsonErrors(async () => {
|
|
2136
|
+
const { setSecret } = await import("./commands/secret.js");
|
|
2137
|
+
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
2138
|
+
const fromEnv = getHyphenatedArg(args, "from-env");
|
|
2139
|
+
const fromFile = getHyphenatedArg(args, "from-file");
|
|
2140
|
+
if (fromEnv !== undefined && fromFile !== undefined) {
|
|
2141
|
+
throw new UsageError("Pass only one of --from-file or --from-env (or use stdin).", "INVALID_FLAG_VALUE");
|
|
2142
|
+
}
|
|
2143
|
+
const MAX_SECRET_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
2144
|
+
let value;
|
|
2145
|
+
if (fromFile !== undefined) {
|
|
2146
|
+
if (!fs.existsSync(fromFile)) {
|
|
2147
|
+
throw new NotFoundError(`File not found: ${fromFile}`, "FILE_NOT_FOUND");
|
|
2148
|
+
}
|
|
2149
|
+
value = fs.readFileSync(fromFile);
|
|
2150
|
+
if (value.byteLength > MAX_SECRET_BYTES) {
|
|
2151
|
+
throw new UsageError("Secret exceeds the 5 MB limit.");
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
else if (fromEnv !== undefined) {
|
|
2155
|
+
const envVal = process.env[fromEnv];
|
|
2156
|
+
if (envVal === undefined) {
|
|
2157
|
+
throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
|
|
2158
|
+
}
|
|
2159
|
+
value = Buffer.from(envVal, "utf8");
|
|
2160
|
+
}
|
|
2161
|
+
else {
|
|
2162
|
+
if (process.stdin.isTTY) {
|
|
2163
|
+
process.stderr.write(`Enter value for secret "${name}" (Ctrl-D when done):\n`);
|
|
2164
|
+
}
|
|
2165
|
+
let totalBytes = 0;
|
|
2166
|
+
const chunks = [];
|
|
2167
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
2168
|
+
totalBytes += chunk.byteLength;
|
|
2169
|
+
if (totalBytes > MAX_SECRET_BYTES) {
|
|
2170
|
+
throw new UsageError("Secret exceeds the 5 MB limit.");
|
|
2171
|
+
}
|
|
2172
|
+
chunks.push(chunk);
|
|
2173
|
+
}
|
|
2174
|
+
// Strip a single trailing newline so `echo "$TOKEN" | akm secret set`
|
|
2175
|
+
// stores the token without the shell-added newline. Use --from-file for
|
|
2176
|
+
// byte-exact storage of multi-line material (PEM keys, certs).
|
|
2177
|
+
value = Buffer.from(Buffer.concat(chunks).toString("utf8").replace(/\n$/, ""), "utf8");
|
|
2178
|
+
}
|
|
2179
|
+
setSecret(absPath, value);
|
|
2180
|
+
output("secret-set", { ref: makeSecretRef(name, source) });
|
|
2181
|
+
});
|
|
2182
|
+
},
|
|
2183
|
+
});
|
|
2184
|
+
const secretPathCommand = defineCommand({
|
|
2185
|
+
meta: {
|
|
2186
|
+
name: "path",
|
|
2187
|
+
description: "Print the absolute secret file path for the Docker `_FILE` convention, e.g. `MY_SECRET_FILE=$(akm secret path secret:deploy-key)`.",
|
|
2188
|
+
},
|
|
2189
|
+
args: {
|
|
2190
|
+
ref: { type: "positional", description: "Secret ref", required: true },
|
|
2191
|
+
},
|
|
2192
|
+
run({ args }) {
|
|
2193
|
+
return runWithJsonErrors(async () => {
|
|
2194
|
+
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
2195
|
+
if (!fs.existsSync(absPath)) {
|
|
2196
|
+
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
2197
|
+
}
|
|
2198
|
+
process.stdout.write(`${absPath}\n`);
|
|
2199
|
+
});
|
|
2200
|
+
},
|
|
2201
|
+
});
|
|
2202
|
+
const secretRunCommand = defineCommand({
|
|
2203
|
+
meta: {
|
|
2204
|
+
name: "run",
|
|
2205
|
+
description: "Run a command with a secret's value injected into an env var: `akm secret run <ref> <VAR> -- <command>`. The value is set as $VAR in the child process only.",
|
|
2206
|
+
},
|
|
2207
|
+
args: {
|
|
2208
|
+
ref: { type: "positional", description: "Secret ref", required: true },
|
|
2209
|
+
var: { type: "positional", description: "Environment variable name to inject the value into", required: true },
|
|
2210
|
+
},
|
|
2211
|
+
run({ args }) {
|
|
2212
|
+
return runWithJsonErrors(async () => {
|
|
2213
|
+
// Validate the target env var name FIRST (before the command split) so a
|
|
2214
|
+
// dangerous/invalid name is rejected regardless of how the command is
|
|
2215
|
+
// supplied — and so the failure does not depend on argv parsing.
|
|
2216
|
+
const varName = args.var;
|
|
2217
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) {
|
|
2218
|
+
throw new UsageError(`"${varName}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
|
|
2219
|
+
}
|
|
2220
|
+
const { isDangerousVaultKey } = await import("./commands/lint/vault-key-rules.js");
|
|
2221
|
+
if (isDangerousVaultKey(varName)) {
|
|
2222
|
+
throw new UsageError(`Refusing to inject a secret into "${varName}": it is a known process-hijacking variable (e.g. LD_PRELOAD, PATH).`, "INVALID_FLAG_VALUE");
|
|
2223
|
+
}
|
|
2224
|
+
const dashIndex = process.argv.indexOf("--");
|
|
2225
|
+
if (dashIndex < 0 || dashIndex === process.argv.length - 1) {
|
|
2226
|
+
throw new UsageError("Missing command. Usage: akm secret run <ref> <VAR> -- <command>");
|
|
2227
|
+
}
|
|
2228
|
+
const command = process.argv.slice(dashIndex + 1);
|
|
2229
|
+
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
2230
|
+
if (!fs.existsSync(absPath)) {
|
|
2231
|
+
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
2232
|
+
}
|
|
2233
|
+
const { readValue } = await import("./commands/secret.js");
|
|
2234
|
+
const mergedEnv = { ...process.env };
|
|
2235
|
+
mergedEnv[varName] = readValue(absPath).toString("utf8");
|
|
2236
|
+
// Audit trail: record access by ref + var name only — never the value.
|
|
2237
|
+
appendEvent({
|
|
2238
|
+
eventType: "secret_access",
|
|
2239
|
+
ref: makeSecretRef(name, source),
|
|
2240
|
+
metadata: { var: varName },
|
|
2241
|
+
});
|
|
2242
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
2243
|
+
stdio: "inherit",
|
|
2244
|
+
env: mergedEnv,
|
|
2245
|
+
});
|
|
2246
|
+
if (result.error) {
|
|
2247
|
+
const err = result.error;
|
|
2248
|
+
if (err.code === "ENOENT") {
|
|
2249
|
+
throw new NotFoundError(`Command not found: ${command[0]}`, "FILE_NOT_FOUND", `Install '${command[0]}' or add its directory to PATH before invoking 'akm secret run'.`);
|
|
2250
|
+
}
|
|
2251
|
+
if (err.code === "EACCES") {
|
|
2252
|
+
throw new ConfigError(`Command not executable: ${command[0]}`, "STASH_DIR_UNREADABLE", `Add execute permission ('chmod +x ${command[0]}') or invoke via an interpreter.`);
|
|
2253
|
+
}
|
|
2254
|
+
throw err;
|
|
2255
|
+
}
|
|
2256
|
+
process.exit(result.status ?? 0);
|
|
2257
|
+
});
|
|
2258
|
+
},
|
|
2259
|
+
});
|
|
2260
|
+
const secretRemoveCommand = defineCommand({
|
|
2261
|
+
meta: { name: "remove", description: "Remove a secret (and its .sensitive marker, if any)" },
|
|
2262
|
+
args: {
|
|
2263
|
+
ref: { type: "positional", description: "Secret ref", required: true },
|
|
2264
|
+
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
|
|
2265
|
+
},
|
|
2266
|
+
run({ args }) {
|
|
2267
|
+
return runWithJsonErrors(async () => {
|
|
2268
|
+
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
2269
|
+
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
2270
|
+
const confirmed = await confirmDestructive(`Remove secret "${args.ref}"? This cannot be undone.`, {
|
|
2271
|
+
yes: args.yes === true,
|
|
2272
|
+
});
|
|
2273
|
+
if (!confirmed) {
|
|
2274
|
+
process.stderr.write("Aborted.\n");
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
const { removeSecret } = await import("./commands/secret.js");
|
|
2278
|
+
if (!fs.existsSync(absPath)) {
|
|
2279
|
+
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
2280
|
+
}
|
|
2281
|
+
const removed = removeSecret(absPath);
|
|
2282
|
+
output("secret-remove", { ref: makeSecretRef(name, source), removed });
|
|
2283
|
+
});
|
|
2284
|
+
},
|
|
2285
|
+
});
|
|
2286
|
+
const secretCommand = defineCommand({
|
|
2287
|
+
meta: {
|
|
2288
|
+
name: "secret",
|
|
2289
|
+
description: "Manage whole-file secrets (PEM keys, tokens, certs). Names are visible; the file contents are the value and never appear in structured output.",
|
|
2290
|
+
},
|
|
2291
|
+
subCommands: {
|
|
2292
|
+
list: secretListCommand,
|
|
2293
|
+
path: secretPathCommand,
|
|
2294
|
+
run: secretRunCommand,
|
|
2295
|
+
set: secretSetCommand,
|
|
2296
|
+
remove: secretRemoveCommand,
|
|
2297
|
+
},
|
|
2298
|
+
run({ args }) {
|
|
2299
|
+
return runWithJsonErrors(async () => {
|
|
2300
|
+
if (hasSubcommand(args, SECRET_SUBCOMMAND_SET))
|
|
2301
|
+
return;
|
|
2302
|
+
output("secret-list", { secrets: listSecretsRecursive() });
|
|
2303
|
+
});
|
|
2304
|
+
},
|
|
2305
|
+
});
|
|
2054
2306
|
// ── Wiki subcommands ─────────────────────────────────────────────────────────
|
|
2055
2307
|
const wikiCreateCommand = defineCommand({
|
|
2056
2308
|
meta: { name: "create", description: "Scaffold a new wiki under <stashDir>/wikis/<name>/" },
|
|
@@ -3171,11 +3423,11 @@ const tasksCommand = defineCommand({
|
|
|
3171
3423
|
});
|
|
3172
3424
|
},
|
|
3173
3425
|
});
|
|
3174
|
-
const main = defineCommand({
|
|
3426
|
+
export const main = defineCommand({
|
|
3175
3427
|
meta: {
|
|
3176
3428
|
name: "akm",
|
|
3177
3429
|
version: pkgVersion,
|
|
3178
|
-
description: "Agent
|
|
3430
|
+
description: "Agent Knowledge Management — search, show, and manage assets from your stash.",
|
|
3179
3431
|
},
|
|
3180
3432
|
args: {
|
|
3181
3433
|
format: { type: "string", description: "Output format (json|jsonl|text|yaml)", default: "json" },
|
|
@@ -3243,12 +3495,14 @@ const main = defineCommand({
|
|
|
3243
3495
|
hints: hintsCommand,
|
|
3244
3496
|
completions: completionsCommand,
|
|
3245
3497
|
vault: vaultCommand,
|
|
3498
|
+
secret: secretCommand,
|
|
3246
3499
|
wiki: wikiCommand,
|
|
3247
3500
|
tasks: tasksCommand,
|
|
3248
3501
|
},
|
|
3249
3502
|
});
|
|
3250
3503
|
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "show", "get", "set", "unset"]);
|
|
3251
3504
|
const VAULT_SUBCOMMAND_SET = new Set(["list", "path", "run", "create", "set", "unset"]);
|
|
3505
|
+
const SECRET_SUBCOMMAND_SET = new Set(["list", "path", "run", "set", "remove"]);
|
|
3252
3506
|
const WIKI_SUBCOMMAND_SET = new Set([
|
|
3253
3507
|
"create",
|
|
3254
3508
|
"register",
|
|
@@ -3267,62 +3521,69 @@ const EXIT_GENERAL = 1;
|
|
|
3267
3521
|
* fired but no hard failure). Chosen as 4 to avoid colliding with EXIT_GENERAL
|
|
3268
3522
|
* (1) and USAGE (2). CI monitors can map: 0=pass, 4=warn, 1=fail. */
|
|
3269
3523
|
const EXIT_HEALTH_WARN = 4;
|
|
3270
|
-
//
|
|
3271
|
-
//
|
|
3272
|
-
|
|
3273
|
-
//
|
|
3274
|
-
//
|
|
3275
|
-
|
|
3276
|
-
//
|
|
3277
|
-
//
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
// If the old file exists at $XDG_CACHE_HOME/akm/index.db, remove it so the
|
|
3288
|
-
// user isn't confused by a phantom DB. Best-effort; never fatal.
|
|
3289
|
-
try {
|
|
3290
|
-
const oldIndexPath = path.join(getCacheDir(), "index.db");
|
|
3291
|
-
if (fs.existsSync(oldIndexPath)) {
|
|
3292
|
-
fs.rmSync(oldIndexPath, { force: true });
|
|
3293
|
-
fs.rmSync(`${oldIndexPath}-shm`, { force: true });
|
|
3294
|
-
fs.rmSync(`${oldIndexPath}-wal`, { force: true });
|
|
3295
|
-
warn(`Cleaned up stale 0.7.x index from ${oldIndexPath}. Canonical path is now ${getDbPath()}.`);
|
|
3524
|
+
// Only run the CLI when this module is the direct entry point. When it is
|
|
3525
|
+
// imported (e.g. by the in-process test harness in tests/_helpers/cli.ts),
|
|
3526
|
+
// `import.meta.main` is false and we skip all startup side effects (argv
|
|
3527
|
+
// mutation, output-mode init, index cleanup, banner, runMain) so importers
|
|
3528
|
+
// can drive the `main` command themselves without the process exiting.
|
|
3529
|
+
if (import.meta.main) {
|
|
3530
|
+
// citty reads process.argv directly and does not accept a custom argv array,
|
|
3531
|
+
// so we must replace process.argv with the normalized version before runMain.
|
|
3532
|
+
process.argv = normalizeShowArgv(process.argv);
|
|
3533
|
+
// Resolve output mode once at startup from the (normalized) argv and persisted
|
|
3534
|
+
// config. All subsequent output() calls read from this in-memory singleton.
|
|
3535
|
+
// `initOutputMode` can throw a UsageError when --format/--detail values are
|
|
3536
|
+
// invalid; surface it through the same JSON-error path the rest of the CLI uses
|
|
3537
|
+
// rather than letting the raw exception escape with a stack trace.
|
|
3538
|
+
try {
|
|
3539
|
+
applyEarlyStderrFlags(process.argv);
|
|
3540
|
+
initOutputMode(process.argv, loadConfig().output ?? {});
|
|
3296
3541
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
//
|
|
3302
|
-
//
|
|
3303
|
-
//
|
|
3304
|
-
// are interactive (so JSON-output users / CI consumers see nothing extra)
|
|
3305
|
-
// and stays silent for any flag-only invocation citty would handle itself
|
|
3306
|
-
// (--help, --version).
|
|
3307
|
-
(function maybePrintFirstTimeBanner() {
|
|
3308
|
-
const argv = process.argv.slice(2);
|
|
3309
|
-
// Fire only on completely bare `akm` invocation. Any explicit flag or
|
|
3310
|
-
// subcommand means the user knows what they want.
|
|
3311
|
-
if (argv.length > 0)
|
|
3312
|
-
return;
|
|
3313
|
-
if (!process.stderr.isTTY)
|
|
3314
|
-
return;
|
|
3542
|
+
catch (error) {
|
|
3543
|
+
emitJsonError(error);
|
|
3544
|
+
}
|
|
3545
|
+
// One-time cleanup of stale 0.7.x index file at the old cache location.
|
|
3546
|
+
// 0.8.0 moved the index to $XDG_DATA_HOME/akm/index.db (getDataDir()).
|
|
3547
|
+
// If the old file exists at $XDG_CACHE_HOME/akm/index.db, remove it so the
|
|
3548
|
+
// user isn't confused by a phantom DB. Best-effort; never fatal.
|
|
3315
3549
|
try {
|
|
3316
|
-
|
|
3317
|
-
|
|
3550
|
+
const oldIndexPath = path.join(getCacheDir(), "index.db");
|
|
3551
|
+
if (fs.existsSync(oldIndexPath)) {
|
|
3552
|
+
fs.rmSync(oldIndexPath, { force: true });
|
|
3553
|
+
fs.rmSync(`${oldIndexPath}-shm`, { force: true });
|
|
3554
|
+
fs.rmSync(`${oldIndexPath}-wal`, { force: true });
|
|
3555
|
+
warn(`Cleaned up stale 0.7.x index from ${oldIndexPath}. Canonical path is now ${getDbPath()}.`);
|
|
3556
|
+
}
|
|
3318
3557
|
}
|
|
3319
3558
|
catch {
|
|
3320
|
-
//
|
|
3321
|
-
return;
|
|
3559
|
+
// Non-fatal; one-time warning only.
|
|
3322
3560
|
}
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3561
|
+
// First-time-user breadcrumb: when run with no subcommand AND no config
|
|
3562
|
+
// exists yet AND stderr is a TTY, print a friendly pointer to `akm setup`
|
|
3563
|
+
// above citty's auto-generated usage block. Triggers only when stdin/stderr
|
|
3564
|
+
// are interactive (so JSON-output users / CI consumers see nothing extra)
|
|
3565
|
+
// and stays silent for any flag-only invocation citty would handle itself
|
|
3566
|
+
// (--help, --version).
|
|
3567
|
+
(function maybePrintFirstTimeBanner() {
|
|
3568
|
+
const argv = process.argv.slice(2);
|
|
3569
|
+
// Fire only on completely bare `akm` invocation. Any explicit flag or
|
|
3570
|
+
// subcommand means the user knows what they want.
|
|
3571
|
+
if (argv.length > 0)
|
|
3572
|
+
return;
|
|
3573
|
+
if (!process.stderr.isTTY)
|
|
3574
|
+
return;
|
|
3575
|
+
try {
|
|
3576
|
+
if (fs.existsSync(getConfigPath()))
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
catch {
|
|
3580
|
+
// If we can't resolve the config path, assume non-fresh and stay silent.
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
console.error(plainize("👋 First time with akm? Run `akm setup` to get started.\n Docs: https://github.com/itlackey/akm#readme\n"));
|
|
3584
|
+
})();
|
|
3585
|
+
runMain(main);
|
|
3586
|
+
}
|
|
3326
3587
|
// ── Hints (embedded AGENTS.md) ──────────────────────────────────────────────
|
|
3327
3588
|
function loadHints(detail = "normal") {
|
|
3328
3589
|
const filename = detail === "full" ? "AGENTS.full.md" : "AGENTS.md";
|