akm-cli 0.9.0-beta.54 → 0.9.0-beta.56

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.
Files changed (103) hide show
  1. package/dist/cli.js +5 -3
  2. package/dist/commands/agent/contribute-cli.js +2 -3
  3. package/dist/commands/env/env-cli.js +187 -202
  4. package/dist/commands/env/secret-cli.js +109 -121
  5. package/dist/commands/feedback-cli.js +152 -155
  6. package/dist/commands/health/advisories.js +151 -0
  7. package/dist/commands/health/improve-metrics.js +754 -0
  8. package/dist/commands/health/llm-usage.js +65 -0
  9. package/dist/commands/health/md-report.js +103 -0
  10. package/dist/commands/health/metrics.js +278 -0
  11. package/dist/commands/health/task-runs.js +135 -0
  12. package/dist/commands/health/types.js +18 -0
  13. package/dist/commands/health/windows.js +196 -0
  14. package/dist/commands/health.js +14 -1624
  15. package/dist/commands/improve/anti-collapse.js +170 -0
  16. package/dist/commands/improve/collapse-detector.js +3 -2
  17. package/dist/commands/improve/consolidate.js +636 -633
  18. package/dist/commands/improve/dedup.js +1 -1
  19. package/dist/commands/improve/distill/content-repair.js +202 -0
  20. package/dist/commands/improve/distill/promote-memory.js +228 -0
  21. package/dist/commands/improve/distill/quality-gate.js +233 -0
  22. package/dist/commands/improve/distill-guards.js +127 -0
  23. package/dist/commands/improve/distill.js +49 -575
  24. package/dist/commands/improve/extract-cli.js +74 -76
  25. package/dist/commands/improve/extract.js +6 -4
  26. package/dist/commands/improve/hot-probation.js +45 -0
  27. package/dist/commands/improve/improve-auto-accept.js +3 -2
  28. package/dist/commands/improve/improve-cli.js +14 -13
  29. package/dist/commands/improve/improve-result-file.js +2 -1
  30. package/dist/commands/improve/improve.js +6 -5
  31. package/dist/commands/improve/loop-stages.js +19 -21
  32. package/dist/commands/improve/preparation.js +4 -2
  33. package/dist/commands/improve/procedural.js +10 -31
  34. package/dist/commands/improve/recombine.js +19 -43
  35. package/dist/commands/improve/reflect.js +1 -1
  36. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  37. package/dist/commands/improve/shared.js +48 -0
  38. package/dist/commands/observability-cli.js +4 -4
  39. package/dist/commands/proposal/drain-policies.js +2 -2
  40. package/dist/commands/proposal/drain.js +1 -1
  41. package/dist/commands/proposal/legacy-import.js +115 -0
  42. package/dist/commands/proposal/proposal-cli.js +3 -3
  43. package/dist/commands/proposal/proposal.js +2 -1
  44. package/dist/commands/proposal/propose.js +1 -1
  45. package/dist/commands/proposal/repository.js +829 -0
  46. package/dist/commands/proposal/validators/proposals.js +5 -920
  47. package/dist/commands/read/remember-cli.js +132 -137
  48. package/dist/commands/read/search-cli.js +1 -1
  49. package/dist/commands/registry-cli.js +76 -87
  50. package/dist/commands/sources/add-cli.js +90 -94
  51. package/dist/commands/sources/history.js +1 -1
  52. package/dist/commands/sources/schema-repair.js +1 -1
  53. package/dist/commands/sources/sources-cli.js +3 -3
  54. package/dist/commands/sources/stash-cli.js +1 -1
  55. package/dist/commands/tasks/tasks-cli.js +1 -2
  56. package/dist/commands/wiki-cli.js +2 -3
  57. package/dist/core/common.js +3 -3
  58. package/dist/core/config/config-schema.js +6 -0
  59. package/dist/core/deep-merge.js +38 -0
  60. package/dist/core/events.js +2 -1
  61. package/dist/core/logs-db.js +8 -13
  62. package/dist/core/paths.js +14 -14
  63. package/dist/core/state-db.js +13 -1140
  64. package/dist/indexer/db/db.js +96 -723
  65. package/dist/indexer/db/entry-mapper.js +41 -0
  66. package/dist/indexer/db/schema.js +516 -0
  67. package/dist/indexer/feedback/utility-policy.js +75 -0
  68. package/dist/indexer/graph/graph-extraction.js +2 -1
  69. package/dist/indexer/index-writer-lock.js +9 -0
  70. package/dist/indexer/indexer.js +78 -23
  71. package/dist/indexer/search/fts-query.js +51 -0
  72. package/dist/integrations/agent/spawn.js +15 -66
  73. package/dist/llm/embedders/cache.js +3 -1
  74. package/dist/output/text/helpers.js +13 -0
  75. package/dist/registry/resolve.js +5 -0
  76. package/dist/scripts/migrate-storage.js +6908 -7447
  77. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +44 -43
  78. package/dist/setup/legacy-config.js +106 -0
  79. package/dist/setup/prompt.js +57 -0
  80. package/dist/setup/providers.js +14 -0
  81. package/dist/setup/semantic-assets.js +124 -0
  82. package/dist/setup/setup.js +24 -1607
  83. package/dist/setup/steps/connection.js +734 -0
  84. package/dist/setup/steps/output.js +31 -0
  85. package/dist/setup/steps/platforms.js +124 -0
  86. package/dist/setup/steps/semantic.js +27 -0
  87. package/dist/setup/steps/sources.js +222 -0
  88. package/dist/setup/steps/stashdir.js +42 -0
  89. package/dist/setup/steps/tasks.js +152 -0
  90. package/dist/storage/repositories/canaries-repository.js +107 -0
  91. package/dist/storage/repositories/consolidation-repository.js +38 -0
  92. package/dist/storage/repositories/embeddings-repository.js +72 -0
  93. package/dist/storage/repositories/events-repository.js +187 -0
  94. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  95. package/dist/storage/repositories/improve-runs-repository.js +130 -0
  96. package/dist/storage/repositories/index-db.js +4 -7
  97. package/dist/storage/repositories/proposals-repository.js +220 -0
  98. package/dist/storage/repositories/recombine-repository.js +213 -0
  99. package/dist/storage/repositories/task-history-repository.js +93 -0
  100. package/dist/storage/sqlite-pragmas.js +3 -3
  101. package/dist/tasks/runner.js +2 -1
  102. package/package.json +1 -1
  103. package/dist/commands/improve/homeostatic.js +0 -497
@@ -0,0 +1,31 @@
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
+ * Setup wizard step: choose the default output format + detail level.
6
+ */
7
+ import * as p from "../../cli/clack.js";
8
+ import { DEFAULT_CONFIG } from "../../core/config/config.js";
9
+ import { prompt } from "../prompt.js";
10
+ export async function stepOutputConfig(current) {
11
+ const defaultOutput = current.output ?? DEFAULT_CONFIG.output ?? { format: "json", detail: "brief" };
12
+ const format = await prompt(() => p.select({
13
+ message: "Default output format?",
14
+ options: [
15
+ { value: "json", label: "json", hint: "structured default" },
16
+ { value: "text", label: "text", hint: "human-readable CLI output" },
17
+ { value: "yaml", label: "yaml", hint: "structured text" },
18
+ ],
19
+ initialValue: defaultOutput.format ?? "json",
20
+ }));
21
+ const detail = await prompt(() => p.select({
22
+ message: "Default output detail level?",
23
+ options: [
24
+ { value: "brief", label: "brief", hint: "compact summaries" },
25
+ { value: "normal", label: "normal", hint: "balanced detail" },
26
+ { value: "full", label: "full", hint: "max available detail" },
27
+ ],
28
+ initialValue: defaultOutput.detail ?? "brief",
29
+ }));
30
+ return { format: format, detail: detail };
31
+ }
@@ -0,0 +1,124 @@
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
+ * Setup wizard steps for agent platforms and agent-CLI selection: detect
6
+ * agent platform config dirs as stash sources, and pick a default agent CLI.
7
+ */
8
+ import * as p from "../../cli/clack.js";
9
+ import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../../integrations/agent/index.js";
10
+ import { detectAgentPlatforms } from "../detect.js";
11
+ import { getCurrentAgentBlock } from "../legacy-config.js";
12
+ import { prompt } from "../prompt.js";
13
+ export async function stepAgentPlatforms(current) {
14
+ const platforms = detectAgentPlatforms();
15
+ if (platforms.length === 0) {
16
+ p.log.info("No agent platform configurations detected.");
17
+ return [];
18
+ }
19
+ const existingPaths = new Set((current.sources ?? []).map((s) => s.path));
20
+ // Filter out platforms already configured
21
+ const newPlatforms = platforms.filter((pl) => !existingPaths.has(pl.path));
22
+ if (newPlatforms.length === 0) {
23
+ p.log.info(`Detected ${platforms.length} agent platform(s), all already configured as stash sources.`);
24
+ return [];
25
+ }
26
+ const selected = await prompt(() => p.multiselect({
27
+ message: "Found agent platform configurations. Add as stash sources?",
28
+ options: newPlatforms.map((pl) => ({
29
+ value: pl.path,
30
+ label: pl.name,
31
+ hint: pl.path,
32
+ })),
33
+ required: false,
34
+ }));
35
+ const entries = [];
36
+ for (const selectedPath of selected) {
37
+ const platform = newPlatforms.find((pl) => pl.path === selectedPath);
38
+ if (platform) {
39
+ entries.push({
40
+ type: "filesystem",
41
+ path: platform.path,
42
+ name: platform.name.toLowerCase().replace(/\s+/g, "-"),
43
+ });
44
+ }
45
+ }
46
+ return entries;
47
+ }
48
+ /**
49
+ * Print a feature capability summary after both connection steps are complete.
50
+ */
51
+ export function printCapabilitySummary(smallModelSkipped, agentConfigured) {
52
+ const lines = ["Setup complete. Here's what's enabled:", ""];
53
+ lines.push(" ✓ akm search, akm curate, akm show — always available");
54
+ if (!smallModelSkipped) {
55
+ lines.push(" ✓ akm index, akm distill, akm remember — small model configured");
56
+ }
57
+ else {
58
+ lines.push(" ✗ akm index, akm distill, akm remember — run `akm setup` to enable");
59
+ }
60
+ if (agentConfigured) {
61
+ lines.push(" ✓ akm propose, akm improve, akm tasks — agent configured");
62
+ }
63
+ else {
64
+ lines.push(" ✗ akm propose, akm improve, akm tasks — run `akm setup` to enable");
65
+ }
66
+ p.note(lines.join("\n"), "Feature Summary");
67
+ }
68
+ export async function stepAgentSelection(current, detections) {
69
+ const currentAgentBlock = getCurrentAgentBlock(current);
70
+ const available = detections.filter((d) => d.available);
71
+ if (available.length === 0) {
72
+ return currentAgentBlock;
73
+ }
74
+ const initialValue = pickDefaultAgentProfile(detections, currentAgentBlock?.default) ?? available[0]?.name;
75
+ const selectedDefault = await prompt(() => p.select({
76
+ message: "Which detected agent CLI should be the default?",
77
+ options: [
78
+ ...available.map((d) => ({
79
+ value: d.name,
80
+ label: d.name,
81
+ hint: d.resolvedPath ?? d.bin,
82
+ })),
83
+ { value: "disabled", label: "Disabled", hint: "do not configure a default agent CLI" },
84
+ ],
85
+ initialValue,
86
+ }));
87
+ if (selectedDefault === "disabled") {
88
+ if (!currentAgentBlock?.profiles && !currentAgentBlock?.timeoutMs) {
89
+ return undefined;
90
+ }
91
+ return {
92
+ ...(currentAgentBlock ?? {}),
93
+ default: undefined,
94
+ };
95
+ }
96
+ return {
97
+ ...(currentAgentBlock ?? {}),
98
+ default: selectedDefault,
99
+ };
100
+ }
101
+ /**
102
+ * Detect installed agent CLIs and produce an updated `agent` config block
103
+ * with a sensible `default` (the first detected profile that the user has
104
+ * not already overridden).
105
+ *
106
+ * Pure-ish: file system / PATH probes are routed through `detectFn` so
107
+ * tests can drive the branches without touching the real PATH.
108
+ *
109
+ * @internal Exported for testing only.
110
+ */
111
+ export function stepAgentCliDetection(current, detectFn = detectAgentCliProfiles) {
112
+ const detections = detectFn(current);
113
+ const currentAgentBlock = getCurrentAgentBlock(current);
114
+ const defaultName = pickDefaultAgentProfile(detections, currentAgentBlock?.default);
115
+ // No installed agents found and no existing config → leave block absent.
116
+ if (!defaultName && !currentAgentBlock) {
117
+ return { detections };
118
+ }
119
+ const agent = {
120
+ ...(currentAgentBlock ?? {}),
121
+ ...(defaultName ? { default: defaultName } : {}),
122
+ };
123
+ return { agent, detections };
124
+ }
@@ -0,0 +1,27 @@
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
+ * Setup wizard step: enable/disable semantic search and decide whether to
6
+ * prepare its assets now.
7
+ */
8
+ import * as p from "../../cli/clack.js";
9
+ import { prompt } from "../prompt.js";
10
+ import { describeSemanticSearchAssets, isRemoteEmbeddingConfig } from "../semantic-assets.js";
11
+ export async function stepSemanticSearch(current, embedding) {
12
+ const enabled = await prompt(() => p.confirm({
13
+ message: "Enable semantic search?",
14
+ initialValue: current.semanticSearchMode !== "off",
15
+ }));
16
+ if (!enabled) {
17
+ return { mode: "off", prepareAssets: false };
18
+ }
19
+ p.note(describeSemanticSearchAssets(embedding).join("\n"), "Semantic Search Assets");
20
+ const prepareAssets = await prompt(() => p.confirm({
21
+ message: isRemoteEmbeddingConfig(embedding)
22
+ ? "Check the embedding endpoint and verify semantic search now?"
23
+ : "Download and verify semantic-search assets now?",
24
+ initialValue: true,
25
+ }));
26
+ return { mode: "auto", prepareAssets };
27
+ }
@@ -0,0 +1,222 @@
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
+ * Setup wizard steps for stash sources and registries: toggle configured
6
+ * sources, pull registry-recommended stashes, add custom sources, and toggle
7
+ * built-in registries.
8
+ */
9
+ import * as p from "../../cli/clack.js";
10
+ import { DEFAULT_CONFIG, getEffectiveRegistries } from "../../core/config/config.js";
11
+ import { prompt, promptOrBack } from "../prompt.js";
12
+ import { loadSetupStashes } from "../registry-stash-loader.js";
13
+ function configuredSourceKey(source) {
14
+ return `${source.type}:${source.path ?? source.url ?? source.name ?? "unknown"}`;
15
+ }
16
+ function describeConfiguredSource(source) {
17
+ const target = source.path ?? source.url ?? "(unknown target)";
18
+ const typeLabel = source.type === "git" ? "Git" : source.type === "filesystem" ? "Filesystem" : source.type;
19
+ return {
20
+ value: configuredSourceKey(source),
21
+ label: source.name ?? target,
22
+ hint: `${typeLabel}: ${target}`,
23
+ };
24
+ }
25
+ function renderConfiguredSourceList(sources) {
26
+ return sources
27
+ .map((source) => {
28
+ const described = describeConfiguredSource(source);
29
+ return `- ${described.label} (${described.hint})`;
30
+ })
31
+ .join("\n");
32
+ }
33
+ function renderInstalledSourceList(installed) {
34
+ return installed.map((entry) => `- ${entry.id} (${entry.source})`).join("\n");
35
+ }
36
+ export async function stepAdditionalSources(currentSources) {
37
+ const sources = [...currentSources];
38
+ let addMore = true;
39
+ while (addMore) {
40
+ const action = await prompt(() => p.select({
41
+ message: "Add another stash source?",
42
+ options: [
43
+ { value: "done", label: "Done — no more sources" },
44
+ { value: "github-repo", label: "GitHub repository", hint: "custom URL" },
45
+ { value: "filesystem", label: "Filesystem path", hint: "local directory" },
46
+ ],
47
+ initialValue: "done",
48
+ }));
49
+ if (action === "done") {
50
+ addMore = false;
51
+ break;
52
+ }
53
+ if (action === "github-repo") {
54
+ const url = await promptOrBack(() => p.text({
55
+ message: "Enter the GitHub repository URL:",
56
+ placeholder: "https://github.com/owner/repo",
57
+ validate: (v) => {
58
+ if (!v?.trim())
59
+ return "URL cannot be empty";
60
+ },
61
+ }));
62
+ if (url === null)
63
+ continue;
64
+ const name = await promptOrBack(() => p.text({
65
+ message: "Give this stash a name (optional):",
66
+ placeholder: "my-repo",
67
+ }));
68
+ if (name === null)
69
+ continue;
70
+ const entry = { type: "git", url: url.trim() };
71
+ if (name.trim())
72
+ entry.name = name.trim();
73
+ if (!sources.some((s) => s.url === entry.url)) {
74
+ sources.push(entry);
75
+ }
76
+ else {
77
+ p.log.warn("This URL is already configured.");
78
+ }
79
+ }
80
+ if (action === "filesystem") {
81
+ const fsPath = await promptOrBack(() => p.text({
82
+ message: "Enter the directory path:",
83
+ placeholder: "/path/to/stash",
84
+ validate: (v) => {
85
+ if (!v?.trim())
86
+ return "Path cannot be empty";
87
+ },
88
+ }));
89
+ if (fsPath === null)
90
+ continue;
91
+ const resolved = fsPath.trim();
92
+ const name = await promptOrBack(() => p.text({
93
+ message: "Give this stash a name (optional):",
94
+ placeholder: "my-stash",
95
+ }));
96
+ if (name === null)
97
+ continue;
98
+ const entry = { type: "filesystem", path: resolved };
99
+ if (name.trim())
100
+ entry.name = name.trim();
101
+ if (!sources.some((s) => s.path === entry.path)) {
102
+ sources.push(entry);
103
+ }
104
+ else {
105
+ p.log.warn("This path is already configured.");
106
+ }
107
+ }
108
+ }
109
+ return sources;
110
+ }
111
+ export async function stepRegistries(current) {
112
+ const defaults = DEFAULT_CONFIG.registries ?? [];
113
+ const currentRegistries = current.registries ?? defaults;
114
+ const defaultUrls = new Set(defaults.map((r) => r.url));
115
+ const enabledUrls = new Set(currentRegistries.filter((r) => r.enabled !== false).map((r) => r.url));
116
+ // Collect custom (non-default) registries to preserve them
117
+ const customRegistries = currentRegistries.filter((r) => !defaultUrls.has(r.url));
118
+ // Show default registries for toggling
119
+ const options = defaults.map((r) => ({
120
+ value: r.url,
121
+ label: r.name ?? r.url,
122
+ hint: r.provider ?? "static index",
123
+ }));
124
+ if (customRegistries.length > 0) {
125
+ p.log.info(`You have ${customRegistries.length} custom registr${customRegistries.length === 1 ? "y" : "ies"} that will be preserved.`);
126
+ }
127
+ const selected = await prompt(() => p.multiselect({
128
+ message: "Which built-in registries should be enabled?",
129
+ options,
130
+ initialValues: options.filter((o) => enabledUrls.has(o.value)).map((o) => o.value),
131
+ }));
132
+ // If all defaults are selected and there are no custom registries,
133
+ // return undefined to use the built-in defaults (avoids pinning)
134
+ const allDefaultsSelected = defaults.every((r) => selected.includes(r.url));
135
+ if (allDefaultsSelected && customRegistries.length === 0) {
136
+ return undefined;
137
+ }
138
+ // Build explicit list: toggled defaults + preserved custom registries
139
+ const result = defaults.map((r) => ({
140
+ ...r,
141
+ enabled: selected.includes(r.url),
142
+ }));
143
+ // Re-add custom registries unchanged
144
+ for (const custom of customRegistries) {
145
+ result.push(custom);
146
+ }
147
+ return result;
148
+ }
149
+ /**
150
+ * @internal Exported for testing only.
151
+ */
152
+ export async function stepAddSources(current, options) {
153
+ const existingSources = [...(current.sources ?? [])];
154
+ const sources = [];
155
+ if (existingSources.length > 0) {
156
+ p.note(renderConfiguredSourceList(existingSources), "Configured stash sources");
157
+ const options = existingSources.map(describeConfiguredSource);
158
+ const selected = await prompt(() => p.multiselect({
159
+ message: "Configured stash sources — uncheck any you want to disable:",
160
+ options,
161
+ initialValues: options.map((option) => option.value),
162
+ required: false,
163
+ }));
164
+ for (const source of existingSources) {
165
+ if (selected.includes(configuredSourceKey(source))) {
166
+ sources.push(source);
167
+ }
168
+ }
169
+ }
170
+ if ((current.installed?.length ?? 0) > 0) {
171
+ p.note(renderInstalledSourceList(current.installed ?? []), "Installed managed stashes (preserved)");
172
+ }
173
+ // ── Registry-driven stash recommendations ─────────────────────────────
174
+ // Fetch available stashes from the official registry (cached, stale-ok).
175
+ // Falls back to the bundled list when the registry is unreachable.
176
+ const registryUrl = getEffectiveRegistries(current)[0]?.url ??
177
+ "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json";
178
+ const availableStashes = await loadSetupStashes(registryUrl);
179
+ if (availableStashes.length > 0) {
180
+ const existingUrls = new Set(sources.map((s) => s.url));
181
+ const stashOptions = availableStashes.map((s) => ({
182
+ value: s.url,
183
+ label: s.name,
184
+ hint: existingUrls.has(s.url) ? `${s.description} (already added)` : s.description || s.source,
185
+ }));
186
+ // Pre-check: already-installed stashes OR default-selected on fresh install
187
+ const initialValues = sources.length > 0
188
+ ? stashOptions.filter((o) => existingUrls.has(o.value)).map((o) => o.value)
189
+ : availableStashes.filter((s) => s.defaultSelected).map((s) => s.url);
190
+ const selectedUrls = await prompt(() => p.multiselect({
191
+ message: availableStashes[0]?.source === "registry"
192
+ ? "Available stashes from the AKM registry — toggle to add or remove:"
193
+ : "Recommended stash sources — toggle to add or remove:",
194
+ options: stashOptions,
195
+ initialValues,
196
+ required: false,
197
+ }));
198
+ // Add newly selected stashes
199
+ for (const url of selectedUrls) {
200
+ if (!existingUrls.has(url)) {
201
+ const entry = availableStashes.find((s) => s.url === url);
202
+ sources.push({ type: "git", url, name: entry?.name });
203
+ existingUrls.add(url);
204
+ }
205
+ }
206
+ // Remove deselected stashes that were previously configured
207
+ for (const entry of availableStashes) {
208
+ if (existingUrls.has(entry.url) && !selectedUrls.includes(entry.url)) {
209
+ const idx = sources.findIndex((s) => s.url === entry.url);
210
+ if (idx !== -1) {
211
+ sources.splice(idx, 1);
212
+ existingUrls.delete(entry.url);
213
+ p.log.info(`Removed ${entry.name}.`);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ if (options?.promptForAdditional === false) {
219
+ return sources;
220
+ }
221
+ return stepAdditionalSources(sources);
222
+ }
@@ -0,0 +1,42 @@
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
+ * Setup wizard step: choose where akm stores its stash (skills, commands,
6
+ * and other assets).
7
+ */
8
+ import * as p from "../../cli/clack.js";
9
+ import { assertSafeStashDir, getDefaultStashDir } from "../../core/paths.js";
10
+ import { prompt } from "../prompt.js";
11
+ export async function stepStashDir(current, options) {
12
+ const defaultDir = options?.preferredDir ?? current.stashDir ?? getDefaultStashDir();
13
+ if (options?.nonInteractive) {
14
+ return defaultDir;
15
+ }
16
+ const choice = await prompt(() => p.select({
17
+ message: "Where should akm store skills, commands, and other assets?",
18
+ options: [
19
+ { value: "default", label: defaultDir, hint: current.stashDir ? "current" : "default" },
20
+ { value: "custom", label: "Enter a custom path..." },
21
+ ],
22
+ }));
23
+ if (choice === "default")
24
+ return defaultDir;
25
+ const customPath = await prompt(() => p.text({
26
+ message: "Enter the stash directory path:",
27
+ placeholder: defaultDir,
28
+ validate: (v) => {
29
+ if (!v?.trim())
30
+ return "Path cannot be empty";
31
+ try {
32
+ assertSafeStashDir(v.trim());
33
+ }
34
+ catch (err) {
35
+ if (err instanceof Error)
36
+ return err.message;
37
+ return "Refused: unsafe stash directory";
38
+ }
39
+ },
40
+ }));
41
+ return customPath.trim();
42
+ }
@@ -0,0 +1,152 @@
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
+ * Setup wizard steps: register the default improve task set and enable/disable
6
+ * scheduled core tasks.
7
+ */
8
+ import * as p from "../../cli/clack.js";
9
+ import { detectServerDefault, isCiEnvironment, registerDefaultTasks } from "../../commands/tasks/default-tasks.js";
10
+ import { akmTasksAdd, akmTasksList, akmTasksSetEnabled, akmTasksSync } from "../../commands/tasks/tasks.js";
11
+ import { saveGitStash } from "../../sources/providers/git.js";
12
+ import { backendNameForPlatform } from "../../tasks/backends/index.js";
13
+ import { listEmbeddedTasks } from "../../tasks/embedded.js";
14
+ import { parseSchedule } from "../../tasks/schedule.js";
15
+ import { prompt } from "../prompt.js";
16
+ /**
17
+ * Normalise a task id the same way `akm tasks` does (strip a trailing `.yml`
18
+ * / `.md` suffix, trim) so the wizard can match embedded template ids against
19
+ * the ids reported by `akmTasksList()`.
20
+ */
21
+ function normaliseTaskIdForMatch(raw) {
22
+ return raw.trim().replace(/\.(yml|md)$/, "");
23
+ }
24
+ /**
25
+ * Setup sub-step (issue #552): idempotently register the default improve task
26
+ * set. Asks a single "Is this a server install?" question (defaulting per
27
+ * platform) to decide whether the nightly sweep is enabled, then delegates to
28
+ * {@link registerDefaultTasks}, which is CI-aware and never duplicates an
29
+ * existing task. Skipped entirely under CI (the registration helper short-
30
+ * circuits, and we never even prompt).
31
+ *
32
+ * Exported for testing.
33
+ */
34
+ export async function stepDefaultImproveTasks(register = registerDefaultTasks) {
35
+ // CI: register nothing and don't prompt.
36
+ if (isCiEnvironment()) {
37
+ p.log.info("CI detected — skipping default improve task registration.");
38
+ return;
39
+ }
40
+ const platformDefault = detectServerDefault();
41
+ const serverInstall = await prompt(() => p.confirm({
42
+ message: "Is this a server install? (enables the nightly quality sweep at 2am)",
43
+ initialValue: platformDefault,
44
+ }));
45
+ const result = await register({ serverInstall: serverInstall === true });
46
+ if (result.skipped)
47
+ return;
48
+ const total = result.created.length + result.existing.length;
49
+ p.log.success(`Default improve tasks registered (${result.created.length} new, ${result.existing.length} already present, ${total} total).`);
50
+ }
51
+ const DEFAULT_SCHEDULED_TASKS_DEPS = {
52
+ list: akmTasksList,
53
+ add: akmTasksAdd,
54
+ setEnabled: akmTasksSetEnabled,
55
+ sync: akmTasksSync,
56
+ gitSync: saveGitStash,
57
+ };
58
+ export async function stepScheduledTasks(deps = DEFAULT_SCHEDULED_TASKS_DEPS) {
59
+ const embedded = listEmbeddedTasks();
60
+ if (embedded.length === 0)
61
+ return;
62
+ // Snapshot current state so we can diff against the user's selection.
63
+ let installed = [];
64
+ try {
65
+ installed = (await deps.list()).tasks;
66
+ }
67
+ catch {
68
+ // A missing/empty tasks dir is fine — treat as nothing installed.
69
+ installed = [];
70
+ }
71
+ const byId = new Map();
72
+ for (const t of installed)
73
+ byId.set(normaliseTaskIdForMatch(t.id), t);
74
+ // Pre-check tasks that are installed AND enabled.
75
+ const preChecked = embedded.filter((e) => byId.get(e.id)?.enabled === true).map((e) => e.id);
76
+ const stateLabel = (e) => {
77
+ const cur = byId.get(e.id);
78
+ if (!cur)
79
+ return "not installed";
80
+ return cur.enabled ? "enabled" : "disabled";
81
+ };
82
+ const selected = await prompt(() => p.multiselect({
83
+ message: "Enable scheduled core tasks? (space to toggle, enter to confirm)",
84
+ required: false,
85
+ initialValues: preChecked,
86
+ options: embedded.map((e) => ({
87
+ value: e.id,
88
+ label: e.label,
89
+ hint: `${e.description} — ${e.schedule} [${stateLabel(e)}]`,
90
+ })),
91
+ }));
92
+ const selectedSet = new Set(selected);
93
+ // Resolve per-task schedule edits for newly-checked, not-yet-installed tasks.
94
+ const scheduleFor = new Map();
95
+ for (const e of embedded) {
96
+ const cur = byId.get(e.id);
97
+ if (selectedSet.has(e.id) && !cur) {
98
+ const edited = await prompt(() => p.text({
99
+ message: `Schedule for ${e.label}?`,
100
+ initialValue: e.schedule,
101
+ validate(value) {
102
+ const candidate = (value ?? "").trim() || e.schedule;
103
+ try {
104
+ parseSchedule(candidate, backendNameForPlatform());
105
+ }
106
+ catch (err) {
107
+ return err instanceof Error ? err.message : "Invalid schedule.";
108
+ }
109
+ return undefined;
110
+ },
111
+ }));
112
+ const sched = (edited ?? "").trim() || e.schedule;
113
+ scheduleFor.set(e.id, sched);
114
+ }
115
+ }
116
+ let syncNeeded = false;
117
+ for (const e of embedded) {
118
+ const cur = byId.get(e.id);
119
+ const checked = selectedSet.has(e.id);
120
+ if (checked && !cur) {
121
+ // New task: copy template into the primary stash + install scheduler entry.
122
+ const schedule = scheduleFor.get(e.id) ?? e.schedule;
123
+ await deps.add({
124
+ id: e.id,
125
+ schedule,
126
+ command: e.command,
127
+ description: e.description,
128
+ });
129
+ syncNeeded = true;
130
+ }
131
+ else if (checked && cur && !cur.enabled) {
132
+ // Present but disabled → re-enable.
133
+ await deps.setEnabled(e.id, true);
134
+ }
135
+ else if (!checked && cur?.enabled) {
136
+ // Previously enabled, now unchecked → disable (keep the stash file).
137
+ await deps.setEnabled(e.id, false);
138
+ }
139
+ // No state change → no action.
140
+ }
141
+ if (syncNeeded) {
142
+ // Reconcile scheduler entries with on-disk YAML, then commit the new file
143
+ // to git (a no-op for non-git stashes).
144
+ await deps.sync();
145
+ try {
146
+ deps.gitSync(undefined, "akm setup: enable scheduled tasks");
147
+ }
148
+ catch {
149
+ // Non-fatal — the task is installed regardless of git sync outcome.
150
+ }
151
+ }
152
+ }