akm-cli 0.8.0-rc2 → 0.8.1
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/{.github/CHANGELOG.md → CHANGELOG.md} +238 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/assets/help/help-accept.md +12 -0
- package/dist/assets/help/help-improve.md +81 -0
- package/dist/{commands → assets}/help/help-proposals.md +7 -4
- package/dist/assets/help/help-reject.md +11 -0
- package/dist/{output → assets/hints}/cli-hints-full.md +60 -32
- package/dist/{output → assets/hints}/cli-hints-short.md +10 -7
- package/dist/assets/profiles/default.json +15 -0
- package/dist/assets/profiles/graph-refresh.json +13 -0
- package/dist/assets/profiles/memory-focus.json +12 -0
- package/dist/assets/profiles/quick.json +15 -0
- package/dist/assets/profiles/thorough.json +15 -0
- package/dist/assets/prompts/extract-session.md +80 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +10 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1557 -147
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +217 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +1042 -55
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +138 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1736 -346
- package/dist/commands/info.js +26 -28
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +86 -16
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +402 -31
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/output/cli-hints.js +7 -4
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +4 -1
- package/dist/tasks/backends/schtasks.js +4 -1
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +6 -3
- package/dist/wiki/wiki.js +4 -1
- package/dist/workflows/authoring.js +4 -1
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/help/help-accept.md +0 -9
- package/dist/commands/help/help-improve.md +0 -53
- package/dist/commands/help/help-reject.md +0 -8
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
- package/dist/llm/prompts/graph-extract-user-prompt.md +0 -12
- /package/dist/{tasks → assets}/backends/launchd-template.xml +0 -0
- /package/dist/{tasks → assets}/backends/schtasks-template.xml +0 -0
- /package/dist/{commands → assets}/help/help-propose.md +0 -0
- /package/dist/{wiki → assets/wiki}/index-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/ingest-workflow-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/log-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/schema-template.md +0 -0
- /package/dist/{workflows → assets/workflows}/workflow-template.md +0 -0
package/dist/core/asset-spec.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import path from "node:path";
|
|
2
5
|
import { buildWorkflowAction } from "../output/renderers";
|
|
3
6
|
import { registerActionBuilder, registerTypeRenderer } from "./asset-registry";
|
|
@@ -63,6 +66,34 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
63
66
|
},
|
|
64
67
|
script: { stashDir: "scripts", ...scriptSpec },
|
|
65
68
|
memory: { stashDir: "memories", ...markdownSpec },
|
|
69
|
+
// Environment assets — whole `.env` files sourced/injected wholesale. Replaces
|
|
70
|
+
// the deprecated `vault` type (see below). Key NAMES + start-of-line comments
|
|
71
|
+
// are surfaced as metadata; values are never read for indexing.
|
|
72
|
+
env: {
|
|
73
|
+
stashDir: "env",
|
|
74
|
+
isRelevantFile: (fileName) => fileName === ".env" || fileName.endsWith(".env"),
|
|
75
|
+
toCanonicalName: (typeRoot, filePath) => {
|
|
76
|
+
const rel = toPosix(path.relative(typeRoot, filePath));
|
|
77
|
+
const fileName = path.basename(rel);
|
|
78
|
+
// Treat ".env" as the "default" env; "<name>.env" → "<name>"
|
|
79
|
+
if (fileName === ".env") {
|
|
80
|
+
const dir = path.dirname(rel);
|
|
81
|
+
return dir === "." || dir === "" ? "default" : `${dir}/default`;
|
|
82
|
+
}
|
|
83
|
+
const stripped = rel.endsWith(".env") ? rel.slice(0, -4) : rel;
|
|
84
|
+
return stripped;
|
|
85
|
+
},
|
|
86
|
+
toAssetPath: (typeRoot, name) => {
|
|
87
|
+
if (name === "default")
|
|
88
|
+
return path.join(typeRoot, ".env");
|
|
89
|
+
return path.join(typeRoot, name.endsWith(".env") ? name : `${name}.env`);
|
|
90
|
+
},
|
|
91
|
+
rendererName: "env-file",
|
|
92
|
+
actionBuilder: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (values never reach stdout); akm env export ${ref} --out <file> -> write a sourceable script to a file`,
|
|
93
|
+
},
|
|
94
|
+
// DEPRECATED in 0.8.0, removed in 0.9.0 — use `env` instead. Retained so the
|
|
95
|
+
// frozen `vaults/` copy left by the migration still resolves and so existing
|
|
96
|
+
// `vault:` refs keep working through the deprecation window.
|
|
66
97
|
vault: {
|
|
67
98
|
stashDir: "vaults",
|
|
68
99
|
isRelevantFile: (fileName) => fileName === ".env" || fileName.endsWith(".env"),
|
|
@@ -83,7 +114,23 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
83
114
|
return path.join(typeRoot, name.endsWith(".env") ? name : `${name}.env`);
|
|
84
115
|
},
|
|
85
116
|
rendererName: "vault-env",
|
|
86
|
-
actionBuilder: (ref) => `akm show ${ref} -> inspect
|
|
117
|
+
actionBuilder: (ref) => `DEPRECATED (use env): akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with injected env`,
|
|
118
|
+
},
|
|
119
|
+
// Secrets — a single sensitive value used on its own for authentication (a
|
|
120
|
+
// PEM key, API token, TLS cert). Unlike `env` (a group of related .env
|
|
121
|
+
// configuration), the ENTIRE file is the one secret value — there is no safe
|
|
122
|
+
// region to parse, so only the filename is ever surfaced as metadata. The
|
|
123
|
+
// value reaches a command only via `akm secret run` (injected into a child
|
|
124
|
+
// env var) or `akm secret path` (Docker `_FILE` convention). A secret is any
|
|
125
|
+
// regular file under `secrets/` except `.lock`/`.sensitive` sidecars; the
|
|
126
|
+
// canonical name preserves the natural filename (e.g. `id_rsa`, `team/deploy.key`).
|
|
127
|
+
secret: {
|
|
128
|
+
stashDir: "secrets",
|
|
129
|
+
isRelevantFile: (fileName) => !fileName.endsWith(".lock") && !fileName.endsWith(".sensitive"),
|
|
130
|
+
toCanonicalName: (typeRoot, filePath) => toPosix(path.relative(typeRoot, filePath)),
|
|
131
|
+
toAssetPath: (typeRoot, name) => path.join(typeRoot, name),
|
|
132
|
+
rendererName: "secret-file",
|
|
133
|
+
actionBuilder: (ref) => `akm show ${ref} -> name only (value never shown); akm secret path ${ref} -> file path; akm secret run ${ref} <VAR> -- <command> -> run with value injected into $VAR`,
|
|
87
134
|
},
|
|
88
135
|
wiki: {
|
|
89
136
|
stashDir: "wikis",
|
|
@@ -104,12 +151,21 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
104
151
|
actionBuilder: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
|
|
105
152
|
},
|
|
106
153
|
// Scheduled tasks. A task file pairs a cron-style schedule with a target
|
|
107
|
-
// (workflow ref or
|
|
108
|
-
// scheduler (cron / launchd / schtasks). Stored
|
|
154
|
+
// (workflow ref, prompt, or command) that `akm tasks` registers with the
|
|
155
|
+
// OS-native scheduler (cron / launchd / schtasks). Stored as pure YAML
|
|
156
|
+
// under <stash>/tasks/<id>.yml.
|
|
109
157
|
task: {
|
|
110
158
|
stashDir: "tasks",
|
|
111
|
-
|
|
112
|
-
|
|
159
|
+
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".yml",
|
|
160
|
+
toCanonicalName: (typeRoot, filePath) => {
|
|
161
|
+
const rel = toPosix(path.relative(typeRoot, filePath));
|
|
162
|
+
return rel.endsWith(".yml") ? rel.slice(0, -4) : rel;
|
|
163
|
+
},
|
|
164
|
+
toAssetPath: (typeRoot, name) => {
|
|
165
|
+
const withExt = name.endsWith(".yml") ? name : `${name}.yml`;
|
|
166
|
+
return path.join(typeRoot, withExt);
|
|
167
|
+
},
|
|
168
|
+
rendererName: "task-yaml",
|
|
113
169
|
actionBuilder: buildTaskAction,
|
|
114
170
|
},
|
|
115
171
|
};
|
package/dist/core/common.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import crypto from "node:crypto";
|
|
1
5
|
import fs from "node:fs";
|
|
2
6
|
import path from "node:path";
|
|
3
7
|
import { TYPE_DIRS } from "./asset-spec";
|
|
4
8
|
import { ConfigError } from "./errors";
|
|
5
9
|
import { getConfigPath, getDefaultStashDir } from "./paths";
|
|
10
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
11
|
+
export const ASSET_TYPES = [
|
|
12
|
+
"skill",
|
|
13
|
+
"command",
|
|
14
|
+
"agent",
|
|
15
|
+
"knowledge",
|
|
16
|
+
"workflow",
|
|
17
|
+
"script",
|
|
18
|
+
"memory",
|
|
19
|
+
"env",
|
|
20
|
+
"vault",
|
|
21
|
+
"secret",
|
|
22
|
+
"wiki",
|
|
23
|
+
"lesson",
|
|
24
|
+
];
|
|
25
|
+
export const ASSET_TYPE_SET = new Set(ASSET_TYPES);
|
|
6
26
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
7
27
|
export const IS_WINDOWS = process.platform === "win32";
|
|
8
28
|
export function isHttpUrl(value) {
|
|
@@ -28,6 +48,15 @@ export function filterNonEmptyStrings(value) {
|
|
|
28
48
|
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
29
49
|
}
|
|
30
50
|
// ── Validators ──────────────────────────────────────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* Returns true if `type` is a known asset type — either a built-in from
|
|
53
|
+
* {@link ASSET_TYPES} or one dynamically registered via `registerAssetType`.
|
|
54
|
+
*
|
|
55
|
+
* The type guard narrows to `AkmAssetType` for all built-in types. Dynamic
|
|
56
|
+
* types (e.g. registered by plugins) are also accepted at runtime, but the
|
|
57
|
+
* type system treats them as `AkmAssetType` via assertion since they are not
|
|
58
|
+
* part of the static union.
|
|
59
|
+
*/
|
|
31
60
|
export function isAssetType(type) {
|
|
32
61
|
return Object.hasOwn(TYPE_DIRS, type);
|
|
33
62
|
}
|
|
@@ -35,14 +64,52 @@ export function isAssetType(type) {
|
|
|
35
64
|
/**
|
|
36
65
|
* Write content to a file atomically via a temp file + rename.
|
|
37
66
|
* Prevents partial-write corruption on crash.
|
|
38
|
-
*
|
|
67
|
+
* The temp file is opened with the target `mode` (default 0o600) from the
|
|
68
|
+
* start, so it is never world-readable even briefly.
|
|
69
|
+
*
|
|
70
|
+
* Durability: fsync'd against the May 2026 config-clobber incident (#472).
|
|
71
|
+
* On ext4 (data=ordered) and NVMe-with-TRIM, a power-loss inside the kernel
|
|
72
|
+
* writeback window could leave the renamed file truncated to zero — defeating
|
|
73
|
+
* the purpose of the atomic rename. We:
|
|
74
|
+
* 1. fdatasync the temp fd before close, so the data is on disk before the
|
|
75
|
+
* rename observes it.
|
|
76
|
+
* 2. fsync the parent directory after rename, so the directory entry change
|
|
77
|
+
* is durable too. Some filesystems (FAT, certain FUSE mounts) don't
|
|
78
|
+
* support directory fsync; we ignore EINVAL/ENOTSUP so atomic writes
|
|
79
|
+
* don't fail on exotic mounts.
|
|
39
80
|
*/
|
|
40
81
|
export function writeFileAtomic(target, content, mode) {
|
|
41
|
-
const tmp = `${target}.tmp.${process.pid}.${
|
|
42
|
-
fs.
|
|
43
|
-
|
|
44
|
-
fs.
|
|
82
|
+
const tmp = `${target}.tmp.${process.pid}.${crypto.randomBytes(8).toString("hex")}`;
|
|
83
|
+
const fd = fs.openSync(tmp, "w", mode ?? 0o600);
|
|
84
|
+
try {
|
|
85
|
+
fs.writeSync(fd, content);
|
|
86
|
+
try {
|
|
87
|
+
fs.fdatasyncSync(fd);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Best-effort: some pseudo-filesystems lack fdatasync. Fall through
|
|
91
|
+
// to closeSync — the rename below still preserves atomicity even if
|
|
92
|
+
// the data isn't durable, and the calling code's retry will recover.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
fs.closeSync(fd);
|
|
97
|
+
}
|
|
45
98
|
fs.renameSync(tmp, target);
|
|
99
|
+
try {
|
|
100
|
+
const dirFd = fs.openSync(path.dirname(target), "r");
|
|
101
|
+
try {
|
|
102
|
+
fs.fsyncSync(dirFd);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
fs.closeSync(dirFd);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Directory fsync is unsupported on FAT, some FUSE mounts, and Windows
|
|
110
|
+
// (where directories cannot be opened for read like POSIX). Silently
|
|
111
|
+
// ignore so writeFileAtomic remains portable.
|
|
112
|
+
}
|
|
46
113
|
}
|
|
47
114
|
/**
|
|
48
115
|
* Resolve the stash directory using a three-level fallback chain:
|
|
@@ -411,6 +478,27 @@ export function stringArray(value) {
|
|
|
411
478
|
* Group an array of values by a string key derived from each element.
|
|
412
479
|
* Returns a `Map` so insertion order within each group is preserved.
|
|
413
480
|
*/
|
|
481
|
+
/**
|
|
482
|
+
* Return true if a process with the given PID is currently alive.
|
|
483
|
+
* Uses `process.kill(pid, 0)` which does not deliver a signal but
|
|
484
|
+
* throws ESRCH when the process does not exist.
|
|
485
|
+
*/
|
|
486
|
+
export function isProcessAlive(pid) {
|
|
487
|
+
try {
|
|
488
|
+
process.kill(pid, 0);
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Convert a number of days to milliseconds. Consolidates the
|
|
497
|
+
* `N * 24 * 60 * 60 * 1000` pattern used throughout the cooldown logic.
|
|
498
|
+
*/
|
|
499
|
+
export function daysToMs(days) {
|
|
500
|
+
return days * 86_400_000;
|
|
501
|
+
}
|
|
414
502
|
export function groupBy(values, keyFn) {
|
|
415
503
|
const groups = new Map();
|
|
416
504
|
for (const value of values) {
|
package/dist/core/concurrent.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* Maps over items concurrently with a pool size limit.
|
|
3
6
|
* Uses Promise.allSettled semantics — one failure does not cancel others.
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Pure I/O helpers for AKM config files.
|
|
6
|
+
*
|
|
7
|
+
* No knowledge of the AkmConfig shape — these functions just read JSON(C) text
|
|
8
|
+
* from disk and write JSON text back atomically. Validation and migration live
|
|
9
|
+
* in `./config.ts` and `./config-migrate.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Split out so the load path is testable without touching the filesystem
|
|
12
|
+
* (`parseConfigText` is pure), and so a single atomic write path serves
|
|
13
|
+
* `saveConfig`, the migrate command, and the setup wizard (#464.c).
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { writeFileAtomic } from "./common";
|
|
18
|
+
import { ConfigError } from "./errors";
|
|
19
|
+
import { probeLock, releaseLock, tryAcquireLockSync } from "./file-lock";
|
|
20
|
+
import { getCacheDir, getConfigDir } from "./paths";
|
|
21
|
+
/**
|
|
22
|
+
* Read the raw text of a config file. Returns `undefined` when the file does
|
|
23
|
+
* not exist (legitimate cold-start). Other I/O errors propagate.
|
|
24
|
+
*/
|
|
25
|
+
export function readConfigText(configPath) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.readFileSync(configPath, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err.code === "ENOENT")
|
|
31
|
+
return undefined;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse JSON(C) config text into a plain object. Strips `//` and `/* */`
|
|
37
|
+
* comments before parsing.
|
|
38
|
+
*
|
|
39
|
+
* Throws {@link ConfigError} when the text is unparseable or when the root is
|
|
40
|
+
* not a JSON object. Per #458, malformed config text is NOT silently rescued —
|
|
41
|
+
* the caller must surface the parse error.
|
|
42
|
+
*/
|
|
43
|
+
export function parseConfigText(text, sourcePath) {
|
|
44
|
+
const stripped = stripJsonComments(text);
|
|
45
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(stripped);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
52
|
+
throw new ConfigError(`Failed to parse config JSON${where}: ${detail}`, "INVALID_CONFIG_FILE", "Edit the file to fix the JSON syntax error. Comments (// and /* */) are allowed; trailing commas are not.");
|
|
53
|
+
}
|
|
54
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
55
|
+
throw new ConfigError(`Config file${where} must contain a JSON object at the root, got ${describeJsonRoot(parsed)}.`, "INVALID_CONFIG_FILE");
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
function describeJsonRoot(value) {
|
|
60
|
+
if (value === null)
|
|
61
|
+
return "null";
|
|
62
|
+
if (Array.isArray(value))
|
|
63
|
+
return "an array";
|
|
64
|
+
if (typeof value === "string")
|
|
65
|
+
return "a string";
|
|
66
|
+
if (typeof value === "number")
|
|
67
|
+
return "a number";
|
|
68
|
+
if (typeof value === "boolean")
|
|
69
|
+
return "a boolean";
|
|
70
|
+
return typeof value;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Atomically write a config object to disk as pretty-printed JSON. Routes
|
|
74
|
+
* through {@link writeFileAtomic} so partial writes can never corrupt the
|
|
75
|
+
* config file (#464.c).
|
|
76
|
+
*/
|
|
77
|
+
export function writeConfigAtomic(configPath, config) {
|
|
78
|
+
writeFileAtomic(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
79
|
+
}
|
|
80
|
+
/** Maximum number of timestamped config backups to retain (#459). */
|
|
81
|
+
const MAX_CONFIG_BACKUPS = 5;
|
|
82
|
+
/**
|
|
83
|
+
* Snapshot the current config file to `<cacheDir>/config-backups/`. Writes
|
|
84
|
+
* both a timestamped copy and a `config.latest.json` pointer, then prunes the
|
|
85
|
+
* timestamped set to {@link MAX_CONFIG_BACKUPS} most-recent entries.
|
|
86
|
+
*
|
|
87
|
+
* No-op when the source file does not exist (cold-start safe).
|
|
88
|
+
*/
|
|
89
|
+
export function backupExistingConfig(configPath) {
|
|
90
|
+
if (!fs.existsSync(configPath))
|
|
91
|
+
return;
|
|
92
|
+
const backupDir = path.join(getCacheDir(), "config-backups");
|
|
93
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
94
|
+
const timestamp = new Date().toISOString().replace(/[.:]/g, "-");
|
|
95
|
+
fs.copyFileSync(configPath, path.join(backupDir, `config-${timestamp}.json`));
|
|
96
|
+
fs.copyFileSync(configPath, path.join(backupDir, "config.latest.json"));
|
|
97
|
+
pruneOldBackups(backupDir);
|
|
98
|
+
}
|
|
99
|
+
function pruneOldBackups(backupDir) {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = fs.readdirSync(backupDir);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const timestamped = entries
|
|
108
|
+
.filter((n) => n.startsWith("config-") && n.endsWith(".json") && n !== "config.latest.json")
|
|
109
|
+
.map((name) => {
|
|
110
|
+
const full = path.join(backupDir, name);
|
|
111
|
+
let mtime = 0;
|
|
112
|
+
try {
|
|
113
|
+
mtime = fs.statSync(full).mtimeMs;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Unreadable — sorts to the end via mtime 0.
|
|
117
|
+
}
|
|
118
|
+
return { path: full, mtime };
|
|
119
|
+
})
|
|
120
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
121
|
+
for (const stale of timestamped.slice(MAX_CONFIG_BACKUPS)) {
|
|
122
|
+
try {
|
|
123
|
+
fs.unlinkSync(stale.path);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Best-effort prune; next save will retry.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── Config write lock ────────────────────────────────────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* Path to the config write sentinel (`config.json.lck` in $CONFIG).
|
|
133
|
+
*
|
|
134
|
+
* Placed next to config.json so the lock scope is obvious and the path is
|
|
135
|
+
* predictable for debugging. Uses $CONFIG (not $DATA) because config.json
|
|
136
|
+
* itself lives in $CONFIG — they should fail together if the dir is read-only.
|
|
137
|
+
*/
|
|
138
|
+
function getConfigLockPath() {
|
|
139
|
+
return path.join(getConfigDir(), "config.json.lck");
|
|
140
|
+
}
|
|
141
|
+
const CONFIG_LOCK_MAX_RETRIES = 10;
|
|
142
|
+
const CONFIG_LOCK_RETRY_DELAY_MS = 50;
|
|
143
|
+
/**
|
|
144
|
+
* Acquire an exclusive sentinel around config writes.
|
|
145
|
+
*
|
|
146
|
+
* Returns a release function. Best-effort: when all retries are exhausted the
|
|
147
|
+
* write proceeds unlocked rather than erroring (same posture as lockfile.ts).
|
|
148
|
+
*/
|
|
149
|
+
export function acquireConfigLock() {
|
|
150
|
+
const lockPath = getConfigLockPath();
|
|
151
|
+
try {
|
|
152
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Directory already exists or unwritable — let the write fail naturally.
|
|
156
|
+
}
|
|
157
|
+
for (let attempt = 0; attempt < CONFIG_LOCK_MAX_RETRIES; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
if (tryAcquireLockSync(lockPath, String(process.pid))) {
|
|
160
|
+
return () => releaseLock(lockPath);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Non-EEXIST error (permissions, etc.) — bail out and proceed unlocked.
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (probeLock(lockPath).state === "stale") {
|
|
168
|
+
releaseLock(lockPath);
|
|
169
|
+
continue; // Reclaimed — retry immediately.
|
|
170
|
+
}
|
|
171
|
+
if (attempt < CONFIG_LOCK_MAX_RETRIES - 1) {
|
|
172
|
+
// Busy spin (synchronous) — config writes are fast.
|
|
173
|
+
const deadline = Date.now() + CONFIG_LOCK_RETRY_DELAY_MS;
|
|
174
|
+
while (Date.now() < deadline) {
|
|
175
|
+
// spin
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Best-effort: proceed without lock.
|
|
180
|
+
return () => { };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Run `fn` inside the config write lock. Always releases the lock.
|
|
184
|
+
*/
|
|
185
|
+
export function withConfigLock(fn) {
|
|
186
|
+
const release = acquireConfigLock();
|
|
187
|
+
try {
|
|
188
|
+
return fn();
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
release();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// ── Unified diff helper ──────────────────────────────────────────────────────
|
|
195
|
+
/**
|
|
196
|
+
* Produce a minimal unified diff between `before` and `after` text.
|
|
197
|
+
* Uses LCS-based diff with 2-line context. Returns an empty string when the
|
|
198
|
+
* inputs are identical. `label` is used as the path in the diff header.
|
|
199
|
+
*
|
|
200
|
+
* Designed for config files (typically < 200 lines). O(m*n) in line count.
|
|
201
|
+
*/
|
|
202
|
+
export function unifiedDiff(before, after, label) {
|
|
203
|
+
if (before === after)
|
|
204
|
+
return "";
|
|
205
|
+
const a = before.split("\n");
|
|
206
|
+
const b = after.split("\n");
|
|
207
|
+
const eqPairs = lcsLinePairs(a, b);
|
|
208
|
+
const CONTEXT = 2;
|
|
209
|
+
const ops = [];
|
|
210
|
+
let ai = 0;
|
|
211
|
+
let bi = 0;
|
|
212
|
+
let pi = 0;
|
|
213
|
+
while (ai < a.length || bi < b.length) {
|
|
214
|
+
const eq = eqPairs[pi];
|
|
215
|
+
if (eq && eq.ai === ai && eq.bi === bi) {
|
|
216
|
+
ops.push({ type: "eq", line: a[ai], ai, bi });
|
|
217
|
+
ai++;
|
|
218
|
+
bi++;
|
|
219
|
+
pi++;
|
|
220
|
+
}
|
|
221
|
+
else if (ai < a.length && (!eq || ai < eq.ai)) {
|
|
222
|
+
ops.push({ type: "del", line: a[ai], ai, bi });
|
|
223
|
+
ai++;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
ops.push({ type: "add", line: b[bi], ai, bi });
|
|
227
|
+
bi++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Find changed op indices
|
|
231
|
+
const changed = new Set(ops.map((o, i) => (o.type !== "eq" ? i : -1)).filter((i) => i >= 0));
|
|
232
|
+
if (changed.size === 0)
|
|
233
|
+
return "";
|
|
234
|
+
// Determine which equal lines to include as context
|
|
235
|
+
const include = new Set();
|
|
236
|
+
for (const ci of changed) {
|
|
237
|
+
for (let k = Math.max(0, ci - CONTEXT); k <= Math.min(ops.length - 1, ci + CONTEXT); k++) {
|
|
238
|
+
include.add(k);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Collect hunks
|
|
242
|
+
const header = [`--- ${label} (before)`, `+++ ${label} (after)`];
|
|
243
|
+
const out = [];
|
|
244
|
+
let hunkOps = [];
|
|
245
|
+
let prevIncluded = false;
|
|
246
|
+
function flushHunk() {
|
|
247
|
+
if (hunkOps.length === 0)
|
|
248
|
+
return;
|
|
249
|
+
const delStart = hunkOps.find((o) => o.type !== "add")?.ai ?? 0;
|
|
250
|
+
const addStart = hunkOps.find((o) => o.type !== "del")?.bi ?? 0;
|
|
251
|
+
const countA = hunkOps.filter((o) => o.type !== "add").length;
|
|
252
|
+
const countB = hunkOps.filter((o) => o.type !== "del").length;
|
|
253
|
+
out.push(`@@ -${delStart + 1},${countA} +${addStart + 1},${countB} @@`);
|
|
254
|
+
for (const op of hunkOps) {
|
|
255
|
+
const ch = op.type === "eq" ? " " : op.type === "del" ? "-" : "+";
|
|
256
|
+
out.push(`${ch}${op.line}`);
|
|
257
|
+
}
|
|
258
|
+
hunkOps = [];
|
|
259
|
+
}
|
|
260
|
+
for (let k = 0; k < ops.length; k++) {
|
|
261
|
+
if (include.has(k)) {
|
|
262
|
+
hunkOps.push(ops[k]);
|
|
263
|
+
prevIncluded = true;
|
|
264
|
+
}
|
|
265
|
+
else if (prevIncluded) {
|
|
266
|
+
flushHunk();
|
|
267
|
+
prevIncluded = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
flushHunk();
|
|
271
|
+
return out.length > 0 ? [...header, ...out].join("\n") : "";
|
|
272
|
+
}
|
|
273
|
+
function lcsLinePairs(a, b) {
|
|
274
|
+
const m = a.length;
|
|
275
|
+
const n = b.length;
|
|
276
|
+
if (m === 0 || n === 0)
|
|
277
|
+
return [];
|
|
278
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
279
|
+
for (let i = 1; i <= m; i++) {
|
|
280
|
+
for (let j = 1; j <= n; j++) {
|
|
281
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const result = [];
|
|
285
|
+
let i = m;
|
|
286
|
+
let j = n;
|
|
287
|
+
while (i > 0 && j > 0) {
|
|
288
|
+
if (a[i - 1] === b[j - 1]) {
|
|
289
|
+
result.unshift({ ai: i - 1, bi: j - 1 });
|
|
290
|
+
i--;
|
|
291
|
+
j--;
|
|
292
|
+
}
|
|
293
|
+
else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
294
|
+
i--;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
j--;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Strip JavaScript-style comments from a JSON string (JSONC support).
|
|
304
|
+
* Handles `//` line comments and `/* */` block comments while preserving
|
|
305
|
+
* comment-like sequences inside quoted strings.
|
|
306
|
+
*/
|
|
307
|
+
export function stripJsonComments(text) {
|
|
308
|
+
let result = "";
|
|
309
|
+
let i = 0;
|
|
310
|
+
let inString = false;
|
|
311
|
+
while (i < text.length) {
|
|
312
|
+
if (inString) {
|
|
313
|
+
if (text[i] === "\\") {
|
|
314
|
+
result += text[i] + (text[i + 1] ?? "");
|
|
315
|
+
i += 2;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (text[i] === '"') {
|
|
319
|
+
inString = false;
|
|
320
|
+
}
|
|
321
|
+
result += text[i];
|
|
322
|
+
i++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (text[i] === '"') {
|
|
326
|
+
inString = true;
|
|
327
|
+
result += text[i];
|
|
328
|
+
i++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (text[i] === "/" && text[i + 1] === "/") {
|
|
332
|
+
while (i < text.length && text[i] !== "\n")
|
|
333
|
+
i++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (text[i] === "/" && text[i + 1] === "*") {
|
|
337
|
+
i += 2;
|
|
338
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
|
|
339
|
+
i++;
|
|
340
|
+
i += 2;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
result += text[i];
|
|
344
|
+
i++;
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|