@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/dist/cli.js ADDED
@@ -0,0 +1,625 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { normalizeAgentId } from "./agents.js";
6
+ import { runSetup, runArea } from "./setup.js";
7
+ import { runManifestConfigure } from "./manifest-configure.js";
8
+ import { runManifestShow } from "./manifest-show.js";
9
+ import { resolveHome, resolveRepoDir } from "./paths.js";
10
+ export async function runCli(argv, env = process.env) {
11
+ const runtime = {
12
+ io: {
13
+ stdout: (message) => console.log(message),
14
+ stderr: (message) => console.error(message),
15
+ },
16
+ spawn: spawnCommand,
17
+ };
18
+ try {
19
+ return await runCliWithRuntime(argv, env, runtime);
20
+ }
21
+ catch (error) {
22
+ if (isPromptExit(error)) {
23
+ runtime.io.stdout("\nAFK setup cancelled. Nothing else was changed from this prompt.");
24
+ return 130;
25
+ }
26
+ throw error;
27
+ }
28
+ }
29
+ async function runCliWithRuntime(argv, env, runtime) {
30
+ const parsed = parseArgs(argv, env);
31
+ if (parsed.version) {
32
+ runtime.io.stdout(`afk ${packageVersion()}`);
33
+ return 0;
34
+ }
35
+ if (parsed.help) {
36
+ runtime.io.stdout(helpText(parsed.commandPath));
37
+ return 0;
38
+ }
39
+ if (parsed.kind === "error") {
40
+ runtime.io.stderr(parsed.error);
41
+ runtime.io.stderr(helpText());
42
+ return 1;
43
+ }
44
+ const { commandPath, options } = parsed;
45
+ const key = commandKey(commandPath);
46
+ if (key === "setup" || key === "setup refresh") {
47
+ return runSetup(runtime, options);
48
+ }
49
+ if (key === "manifests configure") {
50
+ return runManifestConfigure(runtime, options);
51
+ }
52
+ if (key === "manifests show" || key === "manifest show") {
53
+ return runManifestShow(runtime, options);
54
+ }
55
+ const area = commandToArea(commandPath);
56
+ if (area) {
57
+ return runArea(area, runtime, options);
58
+ }
59
+ runtime.io.stderr(`Unknown command: ${key || "(none)"}`);
60
+ runtime.io.stderr(helpText());
61
+ return 1;
62
+ }
63
+ export function isPromptExit(error) {
64
+ return error instanceof Error && error.name === "ExitPromptError";
65
+ }
66
+ const setupOptions = {
67
+ dryRun: "--dry-run Preview changes without applying them",
68
+ yes: "--yes, -y Accept defaults and skip prompts",
69
+ scope: "--scope global|project Choose machine-wide or current-project setup",
70
+ localScope: "--local Alias for --scope project",
71
+ localManifest: "--local Refresh ./afk/manifests instead of global manifests",
72
+ agent: "--agent <agent> Limit agent targets; repeatable",
73
+ source: "--source github|local Load default manifests from GitHub or this checkout",
74
+ ref: "--ref <git-ref> Git ref for default AFK manifest URLs",
75
+ initOnly: "--init-only Create/update local manifests only, then exit",
76
+ empty: "--empty Create empty manifests with --init-only or refresh",
77
+ defaultsSource: "--defaults-source <source> Use and remember a custom remote or local defaults source",
78
+ includeExternal: "--include-external Include external recommended skills when installing skills",
79
+ };
80
+ const setupAreaOptions = [
81
+ setupOptions.dryRun,
82
+ setupOptions.yes,
83
+ setupOptions.scope,
84
+ setupOptions.localScope,
85
+ setupOptions.agent,
86
+ setupOptions.source,
87
+ setupOptions.ref,
88
+ setupOptions.initOnly,
89
+ setupOptions.empty,
90
+ setupOptions.defaultsSource,
91
+ ];
92
+ const commandHelps = {
93
+ setup: {
94
+ title: "AFK setup",
95
+ summary: "Guided setup for rules, skills, MCPs, utilities, and hooks.",
96
+ usage: "afk setup [options]",
97
+ options: [
98
+ setupOptions.dryRun,
99
+ setupOptions.yes,
100
+ setupOptions.scope,
101
+ setupOptions.localScope,
102
+ setupOptions.agent,
103
+ setupOptions.source,
104
+ setupOptions.ref,
105
+ setupOptions.initOnly,
106
+ setupOptions.empty,
107
+ setupOptions.defaultsSource,
108
+ ],
109
+ subcommands: [
110
+ "afk setup refresh Refresh global or project-local AFK manifests",
111
+ "afk setup rules Sync AFK rules into managed agent rule regions",
112
+ "afk setup skills Delegate skill installation to the official skills CLI",
113
+ "afk setup mcps Delegate MCP installation to add-mcp",
114
+ "afk setup utils Install optional developer utilities",
115
+ "afk setup hooks Merge AFK lifecycle hooks into agent hook configs",
116
+ ],
117
+ examples: [
118
+ "afk setup",
119
+ "afk setup --dry-run",
120
+ "afk setup --local",
121
+ "afk setup refresh --defaults-source your-org/dev-kit",
122
+ "afk setup --defaults-source your-org/dev-kit",
123
+ "afk setup --defaults-source ./afk/manifests",
124
+ ],
125
+ },
126
+ "setup refresh": {
127
+ title: "AFK setup refresh",
128
+ summary: "Refresh AFK manifests from the remembered or selected defaults source.",
129
+ usage: "afk setup refresh [options]",
130
+ options: [
131
+ setupOptions.dryRun,
132
+ setupOptions.localManifest,
133
+ setupOptions.source,
134
+ setupOptions.ref,
135
+ setupOptions.empty,
136
+ setupOptions.defaultsSource,
137
+ ],
138
+ examples: [
139
+ "afk setup refresh",
140
+ "afk setup refresh --local",
141
+ "afk setup refresh --defaults-source your-org/dev-kit",
142
+ "afk setup refresh --source local",
143
+ ],
144
+ },
145
+ "setup hooks": {
146
+ title: "AFK setup hooks",
147
+ summary: "Merge selected AFK lifecycle hooks into supported agent hook configs.",
148
+ usage: "afk setup hooks [options]",
149
+ options: setupAreaOptions,
150
+ examples: [
151
+ "afk setup hooks --dry-run",
152
+ "afk setup hooks --yes --agent codex",
153
+ "afk setup hooks --local --agent cursor-local",
154
+ ],
155
+ },
156
+ "setup hooks install": {
157
+ title: "AFK setup hooks",
158
+ summary: "Merge selected AFK lifecycle hooks into supported agent hook configs.",
159
+ usage: "afk setup hooks [options]",
160
+ options: setupAreaOptions,
161
+ examples: [
162
+ "afk setup hooks --dry-run",
163
+ "afk setup hooks --yes --agent codex",
164
+ "afk setup hooks --local --agent cursor-local",
165
+ ],
166
+ },
167
+ "setup rules": {
168
+ title: "AFK setup rules",
169
+ summary: "Sync AFK rules into managed rule regions.",
170
+ usage: "afk setup rules [options]",
171
+ options: setupAreaOptions,
172
+ examples: [
173
+ "afk setup rules --dry-run",
174
+ "afk setup rules --local",
175
+ "afk setup rules --source local",
176
+ ],
177
+ },
178
+ "setup rules sync": {
179
+ title: "AFK setup rules",
180
+ summary: "Sync AFK rules into managed rule regions.",
181
+ usage: "afk setup rules [options]",
182
+ options: setupAreaOptions,
183
+ examples: [
184
+ "afk setup rules --dry-run",
185
+ "afk setup rules --local",
186
+ "afk setup rules --source local",
187
+ ],
188
+ },
189
+ "setup skills": {
190
+ title: "AFK setup skills",
191
+ summary: "Delegate selected skills to the official skills CLI.",
192
+ usage: "afk setup skills [options]",
193
+ options: [
194
+ ...setupAreaOptions,
195
+ setupOptions.includeExternal,
196
+ ],
197
+ examples: [
198
+ "afk setup skills --dry-run",
199
+ "afk setup skills --yes",
200
+ "afk setup skills --local --agent claude",
201
+ ],
202
+ },
203
+ "setup skills install": {
204
+ title: "AFK setup skills",
205
+ summary: "Delegate selected skills to the official skills CLI.",
206
+ usage: "afk setup skills [options]",
207
+ options: [
208
+ ...setupAreaOptions,
209
+ setupOptions.includeExternal,
210
+ ],
211
+ examples: [
212
+ "afk setup skills --dry-run",
213
+ "afk setup skills --yes",
214
+ "afk setup skills --local --agent claude",
215
+ ],
216
+ },
217
+ "setup mcps": {
218
+ title: "AFK setup MCPs",
219
+ summary: "Delegate selected MCP recommendations to add-mcp.",
220
+ usage: "afk setup mcps [options]",
221
+ options: setupAreaOptions,
222
+ examples: [
223
+ "afk setup mcps --dry-run",
224
+ "afk setup mcps --yes",
225
+ "afk setup mcps --local --agent codex",
226
+ ],
227
+ },
228
+ "setup mcps install": {
229
+ title: "AFK setup MCPs",
230
+ summary: "Delegate selected MCP recommendations to add-mcp.",
231
+ usage: "afk setup mcps [options]",
232
+ options: setupAreaOptions,
233
+ examples: [
234
+ "afk setup mcps --dry-run",
235
+ "afk setup mcps --yes",
236
+ "afk setup mcps --local --agent codex",
237
+ ],
238
+ },
239
+ "setup utils": {
240
+ title: "AFK setup utils",
241
+ summary: "Install optional developer utilities and run supported post-install setup.",
242
+ usage: "afk setup utils [options]",
243
+ options: setupAreaOptions,
244
+ examples: [
245
+ "afk setup utils --dry-run",
246
+ "afk setup utils --yes",
247
+ "afk setup utils --local --agent opencode",
248
+ ],
249
+ },
250
+ "setup utils install": {
251
+ title: "AFK setup utils",
252
+ summary: "Install optional developer utilities and run supported post-install setup.",
253
+ usage: "afk setup utils [options]",
254
+ options: setupAreaOptions,
255
+ examples: [
256
+ "afk setup utils --dry-run",
257
+ "afk setup utils --yes",
258
+ "afk setup utils --local --agent opencode",
259
+ ],
260
+ },
261
+ "manifests configure": {
262
+ title: "AFK manifests configure",
263
+ summary: "Interactively author AFK manifest JSON files.",
264
+ usage: "afk manifests configure [options]",
265
+ options: [
266
+ "--local Write to ./afk/manifests for a defaults repo",
267
+ "--from-current Start from existing manifests when present",
268
+ "--dry-run Preview generated files without writing",
269
+ ],
270
+ examples: [
271
+ "afk manifests configure",
272
+ "afk manifests configure --local",
273
+ "afk manifests configure --from-current",
274
+ ],
275
+ },
276
+ "manifests show": {
277
+ title: "AFK manifests show",
278
+ summary: "Show the current local AFK manifest configuration.",
279
+ usage: "afk manifests show [options]",
280
+ options: [
281
+ "--local Show ./afk/manifests instead of global manifests",
282
+ "--rules Show rules manifest",
283
+ "--skills Show skills manifest",
284
+ "--mcp, --mcps Show MCP manifest",
285
+ "--utils Show utilities manifest",
286
+ "--hooks Show hooks manifest",
287
+ "--presets Show presets manifest",
288
+ ],
289
+ examples: [
290
+ "afk manifests show",
291
+ "afk manifests show --local",
292
+ "afk manifests show --rules --skills",
293
+ "afk manifest show --mcp --utils",
294
+ ],
295
+ },
296
+ "manifest show": {
297
+ title: "AFK manifest show",
298
+ summary: "Alias for afk manifests show.",
299
+ usage: "afk manifest show [options]",
300
+ options: [
301
+ "--local Show ./afk/manifests instead of global manifests",
302
+ "--rules Show rules manifest",
303
+ "--skills Show skills manifest",
304
+ "--mcp, --mcps Show MCP manifest",
305
+ "--utils Show utilities manifest",
306
+ "--hooks Show hooks manifest",
307
+ "--presets Show presets manifest",
308
+ ],
309
+ examples: [
310
+ "afk manifest show",
311
+ "afk manifest show --local",
312
+ "afk manifest show --rules --skills",
313
+ ],
314
+ },
315
+ };
316
+ function parseArgs(argv, env) {
317
+ const args = [...argv];
318
+ const commandPath = readCommandPath(args);
319
+ const key = commandKey(commandPath);
320
+ const agents = [];
321
+ const selectedSkillAgentIds = [];
322
+ let dryRun = false;
323
+ let yes = false;
324
+ let setupScope = "global";
325
+ let scopeExplicit = false;
326
+ let includeExternal = false;
327
+ let rulesRef = "main";
328
+ let rulesSource = "manifest";
329
+ let initOnly = false;
330
+ let empty = false;
331
+ const refreshDefaults = key === "setup refresh";
332
+ let defaultsSource = "";
333
+ let manifestLocal = false;
334
+ let manifestConfigureLocal = false;
335
+ let manifestConfigureFromCurrent = false;
336
+ const selectedManifestCategories = [];
337
+ const homeDir = resolveHome(env);
338
+ const repoDir = resolveRepoDir(env);
339
+ const cwd = resolve(process.cwd());
340
+ if (args.includes("--version") || args.includes("-v")) {
341
+ return { version: true, help: false };
342
+ }
343
+ if (commandPath.length === 0 || key === "--help" || key === "-h" || key === "help") {
344
+ return { help: true };
345
+ }
346
+ if (args.includes("--help") || args.includes("-h")) {
347
+ return { help: true, commandPath };
348
+ }
349
+ for (let index = 0; index < args.length; index += 1) {
350
+ const arg = args[index];
351
+ if (!arg) {
352
+ continue;
353
+ }
354
+ if (key === "manifests configure" && arg === "--from-current") {
355
+ manifestConfigureFromCurrent = true;
356
+ continue;
357
+ }
358
+ if ((key === "manifests show" || key === "manifest show") && manifestCategoryFlag(arg)) {
359
+ const category = manifestCategoryFlag(arg);
360
+ if (category && !selectedManifestCategories.includes(category)) {
361
+ selectedManifestCategories.push(category);
362
+ }
363
+ continue;
364
+ }
365
+ if (arg === "--dry-run") {
366
+ dryRun = true;
367
+ continue;
368
+ }
369
+ if (arg === "--yes" || arg === "-y") {
370
+ yes = true;
371
+ continue;
372
+ }
373
+ if (arg === "--local") {
374
+ if (key === "setup refresh") {
375
+ manifestLocal = true;
376
+ continue;
377
+ }
378
+ if (key === "manifests configure") {
379
+ manifestConfigureLocal = true;
380
+ continue;
381
+ }
382
+ setupScope = "project";
383
+ scopeExplicit = true;
384
+ continue;
385
+ }
386
+ if (arg === "--scope") {
387
+ const value = args[index + 1];
388
+ if (value !== "global" && value !== "project") {
389
+ return { help: false, kind: "error", error: `Invalid --scope value: ${value ?? "(missing)"}` };
390
+ }
391
+ setupScope = value;
392
+ scopeExplicit = true;
393
+ index += 1;
394
+ continue;
395
+ }
396
+ if (arg === "--include-external") {
397
+ includeExternal = true;
398
+ continue;
399
+ }
400
+ if (arg === "--init-only") {
401
+ initOnly = true;
402
+ continue;
403
+ }
404
+ if (arg === "--empty") {
405
+ empty = true;
406
+ continue;
407
+ }
408
+ if (arg === "--defaults-source") {
409
+ const value = args[index + 1];
410
+ if (!value) {
411
+ return { help: false, kind: "error", error: "Missing --defaults-source value" };
412
+ }
413
+ defaultsSource = value;
414
+ index += 1;
415
+ continue;
416
+ }
417
+ if (arg === "--ref") {
418
+ const value = args[index + 1];
419
+ if (!value) {
420
+ return { help: false, kind: "error", error: "Missing --ref value" };
421
+ }
422
+ rulesRef = value;
423
+ index += 1;
424
+ continue;
425
+ }
426
+ if (arg === "--source") {
427
+ const value = args[index + 1];
428
+ if (value !== "github" && value !== "local") {
429
+ return { help: false, kind: "error", error: `Invalid --source value: ${value ?? "(missing)"}` };
430
+ }
431
+ rulesSource = value;
432
+ index += 1;
433
+ continue;
434
+ }
435
+ if (arg === "--agent" || arg === "-a") {
436
+ if (isSkillsCommand(key)) {
437
+ const values = readOptionValues(args, index + 1);
438
+ if (values.length === 0) {
439
+ return { help: false, kind: "error", error: "Missing --agent value" };
440
+ }
441
+ for (const value of values) {
442
+ if (!isSkillAgentId(value)) {
443
+ return { help: false, kind: "error", error: `Invalid --agent value for skills: ${value}` };
444
+ }
445
+ selectedSkillAgentIds.push(value);
446
+ }
447
+ index += values.length;
448
+ continue;
449
+ }
450
+ const value = args[index + 1];
451
+ const agent = value ? normalizeAgentId(value) : null;
452
+ if (!agent) {
453
+ return { help: false, kind: "error", error: `Invalid --agent value: ${value ?? "(missing)"}` };
454
+ }
455
+ agents.push(agent);
456
+ index += 1;
457
+ continue;
458
+ }
459
+ return { help: false, kind: "error", error: `Unknown option: ${arg}` };
460
+ }
461
+ return {
462
+ help: false,
463
+ kind: "command",
464
+ commandPath,
465
+ options: {
466
+ agents,
467
+ setupScope,
468
+ scopeExplicit,
469
+ dryRun,
470
+ yes,
471
+ includeExternal,
472
+ selectedSkillIds: [],
473
+ selectedSkillAgentIds,
474
+ selectedMcpIds: [],
475
+ selectedUtilIds: [],
476
+ selectedHookIds: [],
477
+ rulesRef,
478
+ rulesSource,
479
+ initOnly,
480
+ empty,
481
+ refreshDefaults,
482
+ defaultsSource,
483
+ manifestLocal,
484
+ manifestConfigureLocal,
485
+ manifestConfigureFromCurrent,
486
+ selectedManifestCategories,
487
+ homeDir,
488
+ repoDir,
489
+ cwd,
490
+ },
491
+ };
492
+ }
493
+ function readCommandPath(args) {
494
+ const commandPath = [];
495
+ while (args[0] && !args[0].startsWith("-")) {
496
+ commandPath.push(args.shift() ?? "");
497
+ }
498
+ return commandPath.filter(Boolean);
499
+ }
500
+ function commandToArea(commandPath) {
501
+ const key = commandKey(commandPath);
502
+ if (key === "setup rules" || key === "setup rules sync") {
503
+ return "rules";
504
+ }
505
+ if (key === "setup skills" || key === "setup skills install") {
506
+ return "skills";
507
+ }
508
+ if (key === "setup mcps" || key === "setup mcps install") {
509
+ return "mcps";
510
+ }
511
+ if (key === "setup utils" || key === "setup utils install") {
512
+ return "utils";
513
+ }
514
+ if (key === "setup hooks" || key === "setup hooks install") {
515
+ return "hooks";
516
+ }
517
+ return null;
518
+ }
519
+ function isSkillsCommand(key) {
520
+ return key === "setup skills" || key === "setup skills install";
521
+ }
522
+ function isSkillAgentId(value) {
523
+ return value === "claude-code" || value === "kiro-cli" || value === "kilo" || value === "pi" || value === "droid";
524
+ }
525
+ function readOptionValues(args, startIndex) {
526
+ const values = [];
527
+ for (let index = startIndex; index < args.length; index += 1) {
528
+ const value = args[index];
529
+ if (!value || value.startsWith("-")) {
530
+ break;
531
+ }
532
+ values.push(value);
533
+ }
534
+ return values;
535
+ }
536
+ function spawnCommand(command, args, cwd) {
537
+ return new Promise((resolve) => {
538
+ const child = spawn(command, args, {
539
+ cwd,
540
+ stdio: "inherit",
541
+ shell: false,
542
+ });
543
+ child.on("close", (code) => resolve({ code: code ?? 1 }));
544
+ child.on("error", () => resolve({ code: 1 }));
545
+ });
546
+ }
547
+ function helpText(commandPath) {
548
+ const commandHelp = commandPath ? commandHelps[commandKey(commandPath)] : undefined;
549
+ if (commandHelp) {
550
+ return renderCommandHelp(commandHelp);
551
+ }
552
+ return `AFK CLI
553
+
554
+ Guided setup router for AI Field Kit.
555
+
556
+ Usage:
557
+ afk --version
558
+ afk setup [options]
559
+ afk setup refresh [options]
560
+ afk setup rules [options]
561
+ afk setup skills [options]
562
+ afk setup mcps [options]
563
+ afk setup utils [options]
564
+ afk setup hooks [options]
565
+ afk manifests configure [options]
566
+ afk manifests show [options]
567
+
568
+ Run "afk <command> --help" for command-specific options.
569
+
570
+ Agents:
571
+ antigravity, claude, codex, cursor-local, opencode
572
+
573
+ Aliases:
574
+ agy, gemini -> antigravity
575
+ cursor, cursor-ide, cursor-cli -> cursor-local`;
576
+ }
577
+ function commandKey(commandPath = []) {
578
+ return commandPath.join(" ");
579
+ }
580
+ function packageVersion() {
581
+ const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
582
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
583
+ return typeof packageJson.version === "string" ? packageJson.version : "unknown";
584
+ }
585
+ function manifestCategoryFlag(arg) {
586
+ switch (arg) {
587
+ case "--rules":
588
+ return "rules";
589
+ case "--skill":
590
+ case "--skills":
591
+ return "skills";
592
+ case "--mcp":
593
+ case "--mcps":
594
+ return "mcps";
595
+ case "--util":
596
+ case "--utils":
597
+ return "utils";
598
+ case "--hook":
599
+ case "--hooks":
600
+ return "hooks";
601
+ case "--preset":
602
+ case "--presets":
603
+ return "presets";
604
+ default:
605
+ return null;
606
+ }
607
+ }
608
+ function renderCommandHelp(help) {
609
+ const parts = [
610
+ help.title,
611
+ "",
612
+ help.summary,
613
+ "",
614
+ "Usage:",
615
+ ` ${help.usage}`,
616
+ "",
617
+ "Options:",
618
+ ...help.options.map((option) => ` ${option}`),
619
+ ];
620
+ if (help.subcommands && help.subcommands.length > 0) {
621
+ parts.push("", "Subcommands:", ...help.subcommands.map((subcommand) => ` ${subcommand}`));
622
+ }
623
+ parts.push("", "Examples:", ...help.examples.map((example) => ` ${example}`));
624
+ return parts.join("\n");
625
+ }