@logbookfordevs/afk 0.5.2
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/README.md +240 -0
- package/dist/agents.js +59 -0
- package/dist/brand.js +60 -0
- package/dist/cli.js +625 -0
- package/dist/delegates.js +220 -0
- package/dist/fs-utils.js +120 -0
- package/dist/hooks.js +217 -0
- package/dist/index.js +4 -0
- package/dist/interactive.js +273 -0
- package/dist/manifest-configure.js +300 -0
- package/dist/manifest-show.js +207 -0
- package/dist/manifest.js +409 -0
- package/dist/paths.js +17 -0
- package/dist/prompt-ui.js +109 -0
- package/dist/prompt.js +8 -0
- package/dist/rules.js +209 -0
- package/dist/setup.js +225 -0
- package/dist/skills.js +114 -0
- package/dist/terminal-theme.js +37 -0
- package/dist/types.js +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { checkbox, select } from "@inquirer/prompts";
|
|
2
|
+
import { agentIds, hookAgentIds, skillAgentChoices, skillAgentIds, universalSkillAgentLabels } from "./agents.js";
|
|
3
|
+
import { loadHookManifest, loadMcpManifest, loadSkillManifest, loadUtilityManifest } from "./manifest.js";
|
|
4
|
+
import { DEFAULT_CHECKED, afkCheckboxTheme, afkSelectTheme, defaultCheckedDetail, renderPromptStep, resetPromptSteps } from "./prompt-ui.js";
|
|
5
|
+
const setupAreaChoices = [
|
|
6
|
+
{
|
|
7
|
+
name: "Rules",
|
|
8
|
+
value: "rules",
|
|
9
|
+
checked: DEFAULT_CHECKED,
|
|
10
|
+
description: "Sync AFK global rules into supported rule hosts.",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "Skills",
|
|
14
|
+
value: "skills",
|
|
15
|
+
checked: DEFAULT_CHECKED,
|
|
16
|
+
description: "Delegate AFK and recommended skill installs to the skills CLI.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "MCPs",
|
|
20
|
+
value: "mcps",
|
|
21
|
+
checked: DEFAULT_CHECKED,
|
|
22
|
+
description: "Delegate recommended MCP installs to add-mcp.",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "Utils",
|
|
26
|
+
value: "utils",
|
|
27
|
+
checked: DEFAULT_CHECKED,
|
|
28
|
+
description: "Install optional developer utilities AFK recommends.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "Hooks",
|
|
32
|
+
value: "hooks",
|
|
33
|
+
checked: DEFAULT_CHECKED,
|
|
34
|
+
description: "Merge AFK lifecycle hooks into supported agent hook configs.",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
export async function selectSetup(options) {
|
|
38
|
+
if (options.yes) {
|
|
39
|
+
return normalizeSetupSelection({
|
|
40
|
+
areas: setupAreaChoices.map((choice) => choice.value),
|
|
41
|
+
agents: options.agents,
|
|
42
|
+
hookAgents: options.agents,
|
|
43
|
+
setupScope: options.setupScope,
|
|
44
|
+
skillIds: loadSkillManifest(options).items.map((item) => item.id),
|
|
45
|
+
skillAgents: options.selectedSkillAgentIds,
|
|
46
|
+
mcpIds: loadMcpManifest(options).items.map((item) => item.id),
|
|
47
|
+
utilIds: loadUtilityManifest(options).items.map((item) => item.id),
|
|
48
|
+
hookIds: loadHookManifest(options).items.map((item) => item.id),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
resetPromptSteps();
|
|
52
|
+
const setupScope = options.scopeExplicit ? options.setupScope : await selectSetupScope(options.cwd);
|
|
53
|
+
const areas = await selectCheckbox("Choose what AFK should prepare", setupAreaChoices);
|
|
54
|
+
const utilIds = areas.includes("utils") ? await selectUtils(options) : [];
|
|
55
|
+
const needsAgents = areas.some((area) => area === "rules" || area === "mcps") || utilIds.includes("rtk");
|
|
56
|
+
const agents = needsAgents ? await selectAgents(options.agents) : options.agents;
|
|
57
|
+
const skillIds = areas.includes("skills") ? await selectSkills(options) : [];
|
|
58
|
+
const skillAgents = skillIds.length > 0 ? await selectSkillAgents(options) : [];
|
|
59
|
+
const mcpIds = areas.includes("mcps") ? await selectMcps(options) : [];
|
|
60
|
+
const hookIds = areas.includes("hooks") ? await selectHooks(options) : [];
|
|
61
|
+
const hookAgents = hookIds.length > 0 ? await selectHookAgents(options.agents) : options.agents;
|
|
62
|
+
return normalizeSetupSelection({
|
|
63
|
+
areas,
|
|
64
|
+
agents,
|
|
65
|
+
hookAgents,
|
|
66
|
+
setupScope,
|
|
67
|
+
skillIds,
|
|
68
|
+
skillAgents,
|
|
69
|
+
mcpIds,
|
|
70
|
+
utilIds,
|
|
71
|
+
hookIds,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export async function selectRulesSync(options) {
|
|
75
|
+
if (options.yes) {
|
|
76
|
+
return { agents: options.agents };
|
|
77
|
+
}
|
|
78
|
+
resetPromptSteps();
|
|
79
|
+
return { agents: await selectAgents(options.agents) };
|
|
80
|
+
}
|
|
81
|
+
export async function selectSkillsInstall(options) {
|
|
82
|
+
if (options.yes) {
|
|
83
|
+
return {
|
|
84
|
+
skillIds: loadSkillManifest(options).items.map((item) => item.id),
|
|
85
|
+
skillAgents: options.selectedSkillAgentIds,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
resetPromptSteps();
|
|
89
|
+
const skillIds = await selectSkills(options);
|
|
90
|
+
return {
|
|
91
|
+
skillIds,
|
|
92
|
+
skillAgents: skillIds.length > 0 ? await selectSkillAgents(options) : [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export async function selectMcpsInstall(options) {
|
|
96
|
+
if (options.yes) {
|
|
97
|
+
return {
|
|
98
|
+
agents: options.agents,
|
|
99
|
+
mcpIds: loadMcpManifest(options).items.map((item) => item.id),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
resetPromptSteps();
|
|
103
|
+
return {
|
|
104
|
+
agents: await selectAgents(options.agents),
|
|
105
|
+
mcpIds: await selectMcps(options),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export async function selectUtilsInstall(options) {
|
|
109
|
+
if (options.yes) {
|
|
110
|
+
return {
|
|
111
|
+
agents: options.agents,
|
|
112
|
+
utilIds: loadUtilityManifest(options).items.map((item) => item.id),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
resetPromptSteps();
|
|
116
|
+
const utilIds = await selectUtils(options);
|
|
117
|
+
const agents = utilIds.includes("rtk") ? await selectAgents(options.agents) : options.agents;
|
|
118
|
+
return { agents, utilIds };
|
|
119
|
+
}
|
|
120
|
+
export async function selectHooksInstall(options) {
|
|
121
|
+
if (options.yes) {
|
|
122
|
+
return {
|
|
123
|
+
agents: options.agents.filter((agent) => hookAgentIds.includes(agent)),
|
|
124
|
+
hookIds: loadHookManifest(options).items.map((item) => item.id),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
resetPromptSteps();
|
|
128
|
+
return {
|
|
129
|
+
agents: await selectHookAgents(options.agents),
|
|
130
|
+
hookIds: await selectHooks(options),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function normalizeSetupSelection(selection) {
|
|
134
|
+
return {
|
|
135
|
+
...selection,
|
|
136
|
+
hookAgents: selection.hookAgents.filter((agent) => hookAgentIds.includes(agent)),
|
|
137
|
+
skillAgents: selection.skillAgents.filter((agent) => skillAgentIds.includes(agent)),
|
|
138
|
+
areas: selection.areas.filter((area) => {
|
|
139
|
+
if (area === "skills") {
|
|
140
|
+
return selection.skillIds.length > 0;
|
|
141
|
+
}
|
|
142
|
+
if (area === "mcps") {
|
|
143
|
+
return selection.mcpIds.length > 0;
|
|
144
|
+
}
|
|
145
|
+
if (area === "utils") {
|
|
146
|
+
return selection.utilIds.length > 0;
|
|
147
|
+
}
|
|
148
|
+
if (area === "hooks") {
|
|
149
|
+
return selection.hookIds.length > 0 && selection.hookAgents.length > 0;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async function selectSetupScope(cwd) {
|
|
156
|
+
console.log(renderPromptStep("Scope", "Choose whether AFK writes global config or project-local files."));
|
|
157
|
+
return select({
|
|
158
|
+
message: "Where should AFK set things up?",
|
|
159
|
+
choices: [
|
|
160
|
+
{
|
|
161
|
+
name: "Global field kit",
|
|
162
|
+
value: "global",
|
|
163
|
+
description: "Use across all projects on this machine.",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "This project only",
|
|
167
|
+
value: "project",
|
|
168
|
+
description: `Write config into ${cwd} and keep it repo-scoped.`,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
default: "global",
|
|
172
|
+
instructions: {
|
|
173
|
+
navigation: "Use arrow keys to move.",
|
|
174
|
+
pager: "Use arrow keys to reveal more choices.",
|
|
175
|
+
},
|
|
176
|
+
theme: afkSelectTheme,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function selectAgents(preselected) {
|
|
180
|
+
return selectAgentChoices("Choose agent targets", agentIds, preselected);
|
|
181
|
+
}
|
|
182
|
+
async function selectHookAgents(preselected) {
|
|
183
|
+
return selectAgentChoices("Choose hook targets", hookAgentIds, preselected);
|
|
184
|
+
}
|
|
185
|
+
async function selectAgentChoices(message, choices, preselected) {
|
|
186
|
+
const supportedPreselected = preselected.filter((agent) => choices.includes(agent));
|
|
187
|
+
if (supportedPreselected.length > 0) {
|
|
188
|
+
return supportedPreselected;
|
|
189
|
+
}
|
|
190
|
+
return selectCheckbox(message, choices.map((agent) => {
|
|
191
|
+
return {
|
|
192
|
+
name: agent,
|
|
193
|
+
value: agent,
|
|
194
|
+
checked: DEFAULT_CHECKED,
|
|
195
|
+
};
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
async function selectSkills(options) {
|
|
199
|
+
const manifest = loadSkillManifest(options);
|
|
200
|
+
return selectCheckbox("Choose skills to install", manifest.items.map((item) => ({
|
|
201
|
+
name: item.label,
|
|
202
|
+
value: item.id,
|
|
203
|
+
checked: DEFAULT_CHECKED,
|
|
204
|
+
description: item.args.join(" "),
|
|
205
|
+
})));
|
|
206
|
+
}
|
|
207
|
+
async function selectSkillAgents(options) {
|
|
208
|
+
console.log(renderPromptStep("Universal skills", ".agents/skills is always included."));
|
|
209
|
+
for (const line of compactInlineList(universalSkillAgentLabels, 88)) {
|
|
210
|
+
console.log(` ${line}`);
|
|
211
|
+
}
|
|
212
|
+
return selectCheckbox("Choose additional skill agent links", skillAgentChoices.map((agent) => ({
|
|
213
|
+
name: agent.label,
|
|
214
|
+
value: agent.id,
|
|
215
|
+
checked: options.selectedSkillAgentIds.includes(agent.id),
|
|
216
|
+
description: agent.path,
|
|
217
|
+
})));
|
|
218
|
+
}
|
|
219
|
+
function compactInlineList(values, maxLineLength) {
|
|
220
|
+
const lines = [];
|
|
221
|
+
let current = "";
|
|
222
|
+
for (const value of values) {
|
|
223
|
+
const next = current ? `${current}, ${value}` : value;
|
|
224
|
+
if (current && next.length > maxLineLength) {
|
|
225
|
+
lines.push(current);
|
|
226
|
+
current = value;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
current = next;
|
|
230
|
+
}
|
|
231
|
+
if (current) {
|
|
232
|
+
lines.push(current);
|
|
233
|
+
}
|
|
234
|
+
return lines;
|
|
235
|
+
}
|
|
236
|
+
async function selectMcps(options) {
|
|
237
|
+
const manifest = loadMcpManifest(options);
|
|
238
|
+
return selectCheckbox("Choose MCPs to install", manifest.items.map((item) => ({
|
|
239
|
+
name: item.label,
|
|
240
|
+
value: item.id,
|
|
241
|
+
checked: DEFAULT_CHECKED,
|
|
242
|
+
description: item.source,
|
|
243
|
+
})));
|
|
244
|
+
}
|
|
245
|
+
async function selectUtils(options) {
|
|
246
|
+
const manifest = loadUtilityManifest(options);
|
|
247
|
+
return selectCheckbox("Choose utilities to install", manifest.items.map((item) => ({
|
|
248
|
+
name: item.label,
|
|
249
|
+
value: item.id,
|
|
250
|
+
checked: DEFAULT_CHECKED,
|
|
251
|
+
description: item.description,
|
|
252
|
+
})));
|
|
253
|
+
}
|
|
254
|
+
async function selectHooks(options) {
|
|
255
|
+
const manifest = loadHookManifest(options);
|
|
256
|
+
return selectCheckbox("Choose hooks to install", manifest.items.map((item) => ({
|
|
257
|
+
name: item.label,
|
|
258
|
+
value: item.id,
|
|
259
|
+
checked: DEFAULT_CHECKED,
|
|
260
|
+
description: item.description,
|
|
261
|
+
})));
|
|
262
|
+
}
|
|
263
|
+
async function selectCheckbox(message, choices) {
|
|
264
|
+
console.log(renderPromptStep(message, defaultCheckedDetail));
|
|
265
|
+
return checkbox({
|
|
266
|
+
message,
|
|
267
|
+
choices,
|
|
268
|
+
required: false,
|
|
269
|
+
pageSize: 12,
|
|
270
|
+
instructions: "Use space to toggle, enter to continue.",
|
|
271
|
+
theme: afkCheckboxTheme,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { checkbox, confirm, input } from "@inquirer/prompts";
|
|
4
|
+
import { localManifestDir } from "./manifest.js";
|
|
5
|
+
import { DEFAULT_CHECKED, afkCheckboxTheme, afkPromptTheme, defaultCheckedDetail, renderPromptStep, resetPromptSteps } from "./prompt-ui.js";
|
|
6
|
+
const manifestAreaChoices = [
|
|
7
|
+
{ name: "Rules", value: "rules", checked: DEFAULT_CHECKED, description: "Point rules sync at one AGENTS.md source." },
|
|
8
|
+
{ name: "Skills", value: "skills", checked: DEFAULT_CHECKED, description: "List skills delegated to the skills CLI." },
|
|
9
|
+
{ name: "MCPs", value: "mcps", checked: DEFAULT_CHECKED, description: "List MCPs delegated to add-mcp." },
|
|
10
|
+
{ name: "Utils", value: "utils", checked: DEFAULT_CHECKED, description: "List utility install scripts." },
|
|
11
|
+
{ name: "Hooks", value: "hooks", checked: DEFAULT_CHECKED, description: "List lifecycle hooks AFK can merge into agent configs." },
|
|
12
|
+
];
|
|
13
|
+
export async function runManifestConfigure(runtime, options) {
|
|
14
|
+
const outputDir = options.manifestConfigureLocal ? join(options.cwd, "afk", "manifests") : localManifestDir(options.homeDir);
|
|
15
|
+
const existing = options.manifestConfigureFromCurrent ? readExistingManifests(outputDir) : {};
|
|
16
|
+
resetPromptSteps();
|
|
17
|
+
runtime.io.stdout("\nAFK manifests configure");
|
|
18
|
+
runtime.io.stdout(`Writing to: ${outputDir}`);
|
|
19
|
+
runtime.io.stdout(renderPromptStep("Manifest files", defaultCheckedDetail));
|
|
20
|
+
const areas = await checkbox({
|
|
21
|
+
message: "Choose manifests to configure",
|
|
22
|
+
choices: manifestAreaChoices,
|
|
23
|
+
required: false,
|
|
24
|
+
pageSize: 8,
|
|
25
|
+
instructions: "Use space to toggle, enter to continue.",
|
|
26
|
+
theme: afkCheckboxTheme,
|
|
27
|
+
});
|
|
28
|
+
if (areas.length === 0) {
|
|
29
|
+
runtime.io.stdout("\nNothing selected. No manifests changed.");
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
const drafts = {};
|
|
33
|
+
for (const area of areas) {
|
|
34
|
+
runtime.io.stdout(renderPromptStep(areaTitle(area), areaDescription(area)));
|
|
35
|
+
drafts[`${area}.json`] = await configureArea(area, existing);
|
|
36
|
+
}
|
|
37
|
+
runtime.io.stdout("\nManifest preview");
|
|
38
|
+
for (const [filename, content] of Object.entries(drafts)) {
|
|
39
|
+
runtime.io.stdout(`\n--- ${filename} ---\n${content.trimEnd()}`);
|
|
40
|
+
}
|
|
41
|
+
if (options.dryRun) {
|
|
42
|
+
runtime.io.stdout("\nDry run complete. No manifests written.");
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
runtime.io.stdout(renderPromptStep("Write manifests", "Review the preview above, then confirm whether AFK should write the files."));
|
|
46
|
+
const shouldWrite = await askConfirm(`Write ${areas.length} manifest file(s)?`, true);
|
|
47
|
+
if (!shouldWrite) {
|
|
48
|
+
runtime.io.stdout("\nCancelled. No manifests written.");
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
mkdirSync(outputDir, { recursive: true });
|
|
52
|
+
for (const [filename, content] of Object.entries(drafts)) {
|
|
53
|
+
writeFileSync(join(outputDir, filename), content);
|
|
54
|
+
}
|
|
55
|
+
runtime.io.stdout(`\nWrote ${areas.length} manifest file(s) to ${outputDir}.`);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
async function configureArea(area, existing) {
|
|
59
|
+
switch (area) {
|
|
60
|
+
case "rules":
|
|
61
|
+
return configureRules(existing.rules);
|
|
62
|
+
case "skills":
|
|
63
|
+
return configureSkills(existing.skills);
|
|
64
|
+
case "mcps":
|
|
65
|
+
return configureMcps(existing.mcps);
|
|
66
|
+
case "utils":
|
|
67
|
+
return configureUtils(existing.utils);
|
|
68
|
+
case "hooks":
|
|
69
|
+
return configureHooks(existing.hooks);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function configureRules(existing) {
|
|
73
|
+
const url = await askInput({
|
|
74
|
+
message: "Rules raw URL or local path",
|
|
75
|
+
default: existing?.url || "",
|
|
76
|
+
required: true,
|
|
77
|
+
});
|
|
78
|
+
return json({
|
|
79
|
+
version: 1,
|
|
80
|
+
source: inferSource(url),
|
|
81
|
+
url,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function configureSkills(existing) {
|
|
85
|
+
const items = [...(existing?.items ?? [])];
|
|
86
|
+
let defaultSource = existing?.defaultSource ?? "";
|
|
87
|
+
while (true) {
|
|
88
|
+
const source = await askInput({ message: "Skill source repo URL (blank to finish)" });
|
|
89
|
+
if (!source.trim()) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
defaultSource ||= source;
|
|
93
|
+
const skill = await askInput({ message: "Specific skill id/name (optional; blank installs the whole source)" });
|
|
94
|
+
const idSeed = skill.trim() ? skill : source;
|
|
95
|
+
const id = uniqueId(inferId(idSeed), items.map((item) => item.id));
|
|
96
|
+
const label = await askInput({ message: "Skill label", default: inferLabel(id) });
|
|
97
|
+
const isDefault = await askConfirm("Selected by default?", true);
|
|
98
|
+
const autoInvocation = await askConfirm("Allow automatic model invocation?", true);
|
|
99
|
+
items.push({
|
|
100
|
+
id,
|
|
101
|
+
label,
|
|
102
|
+
source,
|
|
103
|
+
args: skill.trim() ? ["--skill", skill.trim()] : [],
|
|
104
|
+
default: isDefault,
|
|
105
|
+
autoInvocation,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return json({
|
|
109
|
+
version: 1,
|
|
110
|
+
defaultSource,
|
|
111
|
+
items,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async function configureMcps(existing) {
|
|
115
|
+
const items = [...(existing?.items ?? [])];
|
|
116
|
+
while (true) {
|
|
117
|
+
const source = await askInput({ message: "MCP source or command (blank to finish)" });
|
|
118
|
+
if (!source.trim()) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
const id = uniqueId(inferId(source), items.map((item) => item.id));
|
|
122
|
+
const label = await askInput({ message: "MCP label", default: inferLabel(id) });
|
|
123
|
+
const name = await askInput({ message: "add-mcp --name value", default: id });
|
|
124
|
+
const extraArgs = await askInput({ message: "Extra add-mcp args (optional)" });
|
|
125
|
+
const isDefault = await askConfirm("Selected by default?", true);
|
|
126
|
+
items.push({
|
|
127
|
+
id,
|
|
128
|
+
label,
|
|
129
|
+
source,
|
|
130
|
+
args: [...(name ? ["--name", name] : []), ...splitArgs(extraArgs)],
|
|
131
|
+
default: isDefault,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return json({ version: 1, items });
|
|
135
|
+
}
|
|
136
|
+
async function configureUtils(existing) {
|
|
137
|
+
const items = [...(existing?.items ?? [])];
|
|
138
|
+
while (true) {
|
|
139
|
+
const installLine = await askInput({ message: "Utility install command (blank to finish)" });
|
|
140
|
+
if (!installLine.trim()) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
const id = uniqueId(inferId(installLine), items.map((item) => item.id));
|
|
144
|
+
const label = await askInput({ message: "Utility label", default: inferLabel(id) });
|
|
145
|
+
const description = await askInput({ message: "Utility description", default: `${label} install script.` });
|
|
146
|
+
const postInstallLine = await askInput({ message: "Post-install command (optional)" });
|
|
147
|
+
const isDefault = await askConfirm("Selected by default?", true);
|
|
148
|
+
items.push({
|
|
149
|
+
id,
|
|
150
|
+
label,
|
|
151
|
+
description,
|
|
152
|
+
install: { command: "sh", args: ["-c", installLine] },
|
|
153
|
+
...(postInstallLine.trim()
|
|
154
|
+
? { postInstall: { command: "sh", args: ["-c", postInstallLine.trim()] } }
|
|
155
|
+
: {}),
|
|
156
|
+
default: isDefault,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return json({ version: 1, items });
|
|
160
|
+
}
|
|
161
|
+
async function configureHooks(existing) {
|
|
162
|
+
const items = [...(existing?.items ?? [])];
|
|
163
|
+
const shouldAddDefault = items.length === 0
|
|
164
|
+
? await askConfirm("Add AFK execution tracking stop check?", true)
|
|
165
|
+
: false;
|
|
166
|
+
if (shouldAddDefault) {
|
|
167
|
+
items.push({
|
|
168
|
+
id: "afk-execution-tracking-stop-check",
|
|
169
|
+
label: "AFK / Execution Tracking Stop Check",
|
|
170
|
+
description: "Nudge the agent once before final handoff when implementation files changed without tracking, implementation notes, or ADR reconciliation.",
|
|
171
|
+
source: "https://raw.githubusercontent.com/logbookfordevs/ai-field-kit/main/hooks/afk-execution-tracking-stop-check.js",
|
|
172
|
+
command: "node",
|
|
173
|
+
args: ["${HOOK_FILE}", "--agent", "${AGENT}"],
|
|
174
|
+
events: ["stop"],
|
|
175
|
+
agents: ["codex", "claude", "cursor-local"],
|
|
176
|
+
default: true,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return json({ version: 1, items });
|
|
180
|
+
}
|
|
181
|
+
async function askInput(config) {
|
|
182
|
+
return input({
|
|
183
|
+
...config,
|
|
184
|
+
theme: afkPromptTheme,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async function askConfirm(message, defaultValue) {
|
|
188
|
+
return confirm({
|
|
189
|
+
message,
|
|
190
|
+
default: defaultValue,
|
|
191
|
+
theme: afkPromptTheme,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function areaTitle(area) {
|
|
195
|
+
switch (area) {
|
|
196
|
+
case "rules":
|
|
197
|
+
return "Rules manifest";
|
|
198
|
+
case "skills":
|
|
199
|
+
return "Skills manifest";
|
|
200
|
+
case "mcps":
|
|
201
|
+
return "MCP manifest";
|
|
202
|
+
case "utils":
|
|
203
|
+
return "Utils manifest";
|
|
204
|
+
case "hooks":
|
|
205
|
+
return "Hooks manifest";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function areaDescription(area) {
|
|
209
|
+
switch (area) {
|
|
210
|
+
case "rules":
|
|
211
|
+
return "Point rules sync at a raw AGENTS.md source.";
|
|
212
|
+
case "skills":
|
|
213
|
+
return "List skills delegated to the official skills CLI.";
|
|
214
|
+
case "mcps":
|
|
215
|
+
return "List MCPs delegated to add-mcp.";
|
|
216
|
+
case "utils":
|
|
217
|
+
return "List utility install scripts and optional post-install commands.";
|
|
218
|
+
case "hooks":
|
|
219
|
+
return "List lifecycle hooks AFK can merge into agent hook configs.";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export function inferId(value) {
|
|
223
|
+
const candidate = filenameStem(value) || value;
|
|
224
|
+
const withoutGit = candidate.replace(/\.git$/i, "");
|
|
225
|
+
const normalized = withoutGit
|
|
226
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
227
|
+
.toLowerCase()
|
|
228
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
229
|
+
.replace(/^-+|-+$/g, "");
|
|
230
|
+
return normalized || "item";
|
|
231
|
+
}
|
|
232
|
+
export function inferLabel(value) {
|
|
233
|
+
const titled = value
|
|
234
|
+
.replace(/\.(md|json|toml|yaml|yml)$/i, "")
|
|
235
|
+
.replace(/^afk-/, "AFK / ")
|
|
236
|
+
.replace(/[-_]+/g, " ")
|
|
237
|
+
.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
238
|
+
.trim();
|
|
239
|
+
return titled.replace(/\b(Api|Mcp|Url|Afk|Cli|Rtk|Pr)\b/g, (match) => match.toUpperCase());
|
|
240
|
+
}
|
|
241
|
+
function filenameStem(value) {
|
|
242
|
+
try {
|
|
243
|
+
const parsed = new URL(value);
|
|
244
|
+
const last = basename(parsed.pathname);
|
|
245
|
+
return last.replace(/\.[^.]+$/, "");
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
const last = basename(value.trim());
|
|
249
|
+
return last.replace(/\.[^.]+$/, "");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function inferSource(value) {
|
|
253
|
+
return /^https:\/\/(raw\.githubusercontent\.com|github\.com)\//.test(value) ? "github" : "local";
|
|
254
|
+
}
|
|
255
|
+
function uniqueId(id, existingIds) {
|
|
256
|
+
if (!existingIds.includes(id)) {
|
|
257
|
+
return id;
|
|
258
|
+
}
|
|
259
|
+
let index = 2;
|
|
260
|
+
while (existingIds.includes(`${id}-${index}`)) {
|
|
261
|
+
index += 1;
|
|
262
|
+
}
|
|
263
|
+
return `${id}-${index}`;
|
|
264
|
+
}
|
|
265
|
+
function splitArgs(value) {
|
|
266
|
+
return value.split(/\s+/).map((item) => item.trim()).filter(Boolean);
|
|
267
|
+
}
|
|
268
|
+
function readExistingManifests(outputDir) {
|
|
269
|
+
const existing = {};
|
|
270
|
+
const skills = readJsonIfExists(join(outputDir, "skills.json"));
|
|
271
|
+
const mcps = readJsonIfExists(join(outputDir, "mcps.json"));
|
|
272
|
+
const rules = readJsonIfExists(join(outputDir, "rules.json"));
|
|
273
|
+
const utils = readJsonIfExists(join(outputDir, "utils.json"));
|
|
274
|
+
const hooks = readJsonIfExists(join(outputDir, "hooks.json"));
|
|
275
|
+
if (skills) {
|
|
276
|
+
existing.skills = skills;
|
|
277
|
+
}
|
|
278
|
+
if (mcps) {
|
|
279
|
+
existing.mcps = mcps;
|
|
280
|
+
}
|
|
281
|
+
if (rules) {
|
|
282
|
+
existing.rules = rules;
|
|
283
|
+
}
|
|
284
|
+
if (utils) {
|
|
285
|
+
existing.utils = utils;
|
|
286
|
+
}
|
|
287
|
+
if (hooks) {
|
|
288
|
+
existing.hooks = hooks;
|
|
289
|
+
}
|
|
290
|
+
return existing;
|
|
291
|
+
}
|
|
292
|
+
function readJsonIfExists(path) {
|
|
293
|
+
if (!existsSync(path)) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
297
|
+
}
|
|
298
|
+
function json(value) {
|
|
299
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
300
|
+
}
|