thomas-agentkit 0.5.3 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.js +213 -245
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,6 +14,13 @@ Install templates into the current directory:
14
14
  npx thomas-agentkit init
15
15
  ```
16
16
 
17
+ If the package is installed in a project, use the local `agentkit` binary:
18
+
19
+ ```bash
20
+ npm install -D thomas-agentkit
21
+ npx agentkit init
22
+ ```
23
+
17
24
  By default, `init` opens a short interactive setup flow in terminals. It asks where to install files, what project type or preset to use, which AI tools you use, which template set to install, how to handle existing files, and whether to personalize repository-level placeholders such as project name, description, issue tracker, docs paths, stack summary, and project commands.
18
25
 
19
26
  Install into another directory:
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { confirm, intro, isCancel, multiselect, select, text } from "@clack/prompts";
3
3
  import { Command } from "commander";
4
- import { constants as fsConstants } from "node:fs";
4
+ import { constants as fsConstants, realpathSync } from "node:fs";
5
5
  import { access, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
@@ -212,49 +212,68 @@ function assertKnownKeys(value, validKeys, name) {
212
212
  }
213
213
  }
214
214
  }
215
+ function optionalConfigString(rawConfig, key) {
216
+ const value = rawConfig[key];
217
+ if (value === undefined) {
218
+ return undefined;
219
+ }
220
+ if (typeof value !== "string") {
221
+ throw new Error(`${configFileName} ${key} must be a string.`);
222
+ }
223
+ return value;
224
+ }
225
+ function readConfigAiTools(value) {
226
+ if (value === undefined) {
227
+ return undefined;
228
+ }
229
+ if (!Array.isArray(value)) {
230
+ throw new Error(`${configFileName} aiTools must be an array.`);
231
+ }
232
+ return value.map((aiTool) => {
233
+ if (typeof aiTool !== "string" || !isAiToolName(aiTool)) {
234
+ throw new Error(`Unknown AI tool "${String(aiTool)}". Valid AI tools: ${validAiTools.join(", ")}.`);
235
+ }
236
+ return aiTool;
237
+ });
238
+ }
239
+ function readConfigPersonalization(value) {
240
+ if (value === undefined) {
241
+ return undefined;
242
+ }
243
+ assertPlainObject(value, `${configFileName} personalization`);
244
+ assertKnownKeys(value, personalizationKeys, `${configFileName} personalization`);
245
+ const personalization = {};
246
+ for (const [key, entry] of Object.entries(value)) {
247
+ if (typeof entry !== "string") {
248
+ throw new Error(`${configFileName} personalization.${key} must be a string.`);
249
+ }
250
+ personalization[key] = entry;
251
+ }
252
+ return personalization;
253
+ }
215
254
  function parseConfig(rawConfig, configPath) {
216
255
  assertPlainObject(rawConfig, configFileName);
217
256
  assertKnownKeys(rawConfig, configKeys, configFileName);
257
+ const preset = optionalConfigString(rawConfig, "preset");
258
+ const templateSet = optionalConfigString(rawConfig, "templateSet");
259
+ const designSystem = optionalConfigString(rawConfig, "designSystem");
218
260
  const config = {};
219
- if (rawConfig.preset !== undefined) {
220
- if (typeof rawConfig.preset !== "string") {
221
- throw new Error(`${configFileName} preset must be a string.`);
222
- }
223
- config.preset = resolvePreset(rawConfig.preset);
261
+ if (preset !== undefined) {
262
+ config.preset = resolvePreset(preset);
224
263
  }
225
- if (rawConfig.templateSet !== undefined) {
226
- if (typeof rawConfig.templateSet !== "string") {
227
- throw new Error(`${configFileName} templateSet must be a string.`);
228
- }
229
- config.templateSet = resolveTemplateSet(rawConfig.templateSet);
264
+ if (templateSet !== undefined) {
265
+ config.templateSet = resolveTemplateSet(templateSet);
230
266
  }
231
- if (rawConfig.aiTools !== undefined) {
232
- if (!Array.isArray(rawConfig.aiTools)) {
233
- throw new Error(`${configFileName} aiTools must be an array.`);
234
- }
235
- config.aiTools = rawConfig.aiTools.map((aiTool) => {
236
- if (typeof aiTool !== "string" || !isAiToolName(aiTool)) {
237
- throw new Error(`Unknown AI tool "${String(aiTool)}". Valid AI tools: ${validAiTools.join(", ")}.`);
238
- }
239
- return aiTool;
240
- });
267
+ const aiTools = readConfigAiTools(rawConfig.aiTools);
268
+ if (aiTools !== undefined) {
269
+ config.aiTools = aiTools;
241
270
  }
242
- if (rawConfig.designSystem !== undefined) {
243
- if (typeof rawConfig.designSystem !== "string") {
244
- throw new Error(`${configFileName} designSystem must be a string.`);
245
- }
246
- config.designSystem = resolveDesignSystem(rawConfig.designSystem);
247
- }
248
- if (rawConfig.personalization !== undefined) {
249
- assertPlainObject(rawConfig.personalization, `${configFileName} personalization`);
250
- assertKnownKeys(rawConfig.personalization, personalizationKeys, `${configFileName} personalization`);
251
- config.personalization = {};
252
- for (const [key, value] of Object.entries(rawConfig.personalization)) {
253
- if (typeof value !== "string") {
254
- throw new Error(`${configFileName} personalization.${key} must be a string.`);
255
- }
256
- config.personalization[key] = value;
257
- }
271
+ if (designSystem !== undefined) {
272
+ config.designSystem = resolveDesignSystem(designSystem);
273
+ }
274
+ const personalization = readConfigPersonalization(rawConfig.personalization);
275
+ if (personalization !== undefined) {
276
+ config.personalization = personalization;
258
277
  }
259
278
  if (config.preset === undefined && rawConfig.preset !== undefined) {
260
279
  throw new Error(`Invalid preset in ${configPath}.`);
@@ -483,161 +502,96 @@ export function personalizeTemplateContent(file, content, values) {
483
502
  }
484
503
  return personalized;
485
504
  }
486
- async function promptForPersonalization(defaults) {
487
- const shouldPersonalize = await confirm({
488
- message: "Personalize template placeholders?",
489
- initialValue: Boolean(defaults),
490
- });
491
- if (isCancel(shouldPersonalize)) {
492
- process.exit(130);
493
- }
494
- if (!shouldPersonalize) {
495
- return undefined;
496
- }
497
- const projectName = await text({
498
- message: "Project name",
499
- placeholder: "[Project Name]",
500
- defaultValue: defaults?.projectName,
501
- });
502
- if (isCancel(projectName)) {
503
- process.exit(130);
504
- }
505
- const projectDescription = await text({
506
- message: "Short project description",
507
- placeholder: "[short project description]",
508
- defaultValue: defaults?.projectDescription,
509
- });
510
- if (isCancel(projectDescription)) {
511
- process.exit(130);
512
- }
513
- const issueTracker = await text({
514
- message: "Issue tracker name",
515
- placeholder: "Linear or GitHub Issues",
516
- defaultValue: defaults?.issueTracker,
517
- });
518
- if (isCancel(issueTracker)) {
519
- process.exit(130);
520
- }
521
- const designSystemPath = await text({
522
- message: "Design system path",
523
- placeholder: "docs/design-system.md",
524
- defaultValue: defaults?.designSystemPath,
525
- });
526
- if (isCancel(designSystemPath)) {
527
- process.exit(130);
528
- }
529
- const briefsPath = await text({
530
- message: "Briefs path",
531
- placeholder: "docs/briefs",
532
- defaultValue: defaults?.briefsPath,
533
- });
534
- if (isCancel(briefsPath)) {
535
- process.exit(130);
505
+ export function shouldPromptForInit(options, streams) {
506
+ if (options.yes) {
507
+ return false;
536
508
  }
537
- const testCommand = await text({
538
- message: "Test command",
539
- placeholder: "npm test",
540
- defaultValue: defaults?.testCommand,
541
- });
542
- if (isCancel(testCommand)) {
543
- process.exit(130);
509
+ if (options.interactive) {
510
+ return true;
544
511
  }
545
- const lintCommand = await text({
546
- message: "Lint command",
547
- placeholder: "npm run lint",
548
- defaultValue: defaults?.lintCommand,
549
- });
550
- if (isCancel(lintCommand)) {
551
- process.exit(130);
512
+ if (options.dryRun) {
513
+ return false;
552
514
  }
553
- const buildCommand = await text({
554
- message: "Build/check command",
555
- placeholder: "npm run build",
556
- defaultValue: defaults?.buildCommand,
557
- });
558
- if (isCancel(buildCommand)) {
515
+ return Boolean(streams.stdin?.isTTY || streams.stdout?.isTTY);
516
+ }
517
+ function resolvePrompt(value) {
518
+ if (isCancel(value)) {
559
519
  process.exit(130);
560
520
  }
561
- const stackSummary = await text({
562
- message: "Stack summary",
563
- placeholder: "Next.js, TypeScript, Tailwind CSS, Vitest",
564
- defaultValue: defaults?.stackSummary,
565
- });
566
- if (isCancel(stackSummary)) {
567
- process.exit(130);
521
+ return value;
522
+ }
523
+ async function promptForTextValue(message, placeholder, defaultValue) {
524
+ return resolvePrompt(await text({ message, placeholder, defaultValue }));
525
+ }
526
+ async function promptForPersonalization(defaults) {
527
+ const shouldPersonalize = resolvePrompt(await confirm({
528
+ message: "Personalize template placeholders?",
529
+ initialValue: Boolean(defaults),
530
+ }));
531
+ if (!shouldPersonalize) {
532
+ return undefined;
568
533
  }
569
534
  return {
570
- projectName,
571
- projectDescription,
572
- issueTracker,
573
- designSystemPath,
574
- briefsPath,
575
- testCommand,
576
- lintCommand,
577
- buildCommand,
578
- stackSummary,
535
+ projectName: await promptForTextValue("Project name", "[Project Name]", defaults?.projectName),
536
+ projectDescription: await promptForTextValue("Short project description", "[short project description]", defaults?.projectDescription),
537
+ issueTracker: await promptForTextValue("Issue tracker name", "Linear or GitHub Issues", defaults?.issueTracker),
538
+ designSystemPath: await promptForTextValue("Design system path", "docs/design-system.md", defaults?.designSystemPath),
539
+ briefsPath: await promptForTextValue("Briefs path", "docs/briefs", defaults?.briefsPath),
540
+ testCommand: await promptForTextValue("Test command", "npm test", defaults?.testCommand),
541
+ lintCommand: await promptForTextValue("Lint command", "npm run lint", defaults?.lintCommand),
542
+ buildCommand: await promptForTextValue("Build/check command", "npm run build", defaults?.buildCommand),
543
+ stackSummary: await promptForTextValue("Stack summary", "Next.js, TypeScript, Tailwind CSS, Vitest", defaults?.stackSummary),
579
544
  };
580
545
  }
581
546
  async function buildInitTemplateContent(file, preset, personalization, designSystem) {
582
547
  const content = await buildTemplateContent(file, preset, designSystem);
583
548
  return personalizeTemplateContent(file, content, personalization);
584
549
  }
550
+ async function installFileIfAllowed(targetDir, file, options, result, getContent) {
551
+ const destination = path.join(targetDir, file);
552
+ if ((await exists(destination)) && !options.force) {
553
+ result.skipped.push(file);
554
+ return;
555
+ }
556
+ result.created.push(file);
557
+ if (options.dryRun) {
558
+ return;
559
+ }
560
+ await mkdir(path.dirname(destination), { recursive: true });
561
+ await writeFile(destination, await getContent());
562
+ }
563
+ async function resolveInitTemplateFiles(options) {
564
+ const allTemplateFiles = await getTemplateFiles();
565
+ if (options.files) {
566
+ return options.files;
567
+ }
568
+ if (options.templateSet || options.aiTools) {
569
+ return getSelectedTemplateFiles(options.templateSet ?? "full", options.aiTools ?? [], allTemplateFiles);
570
+ }
571
+ return allTemplateFiles;
572
+ }
585
573
  async function installTemplates(targetArg, options) {
586
574
  const targetDir = path.resolve(process.cwd(), targetArg || ".");
587
- const allTemplateFiles = await getTemplateFiles();
588
- const files = options.files ??
589
- (options.templateSet || options.aiTools
590
- ? getSelectedTemplateFiles(options.templateSet ?? "full", options.aiTools ?? [], allTemplateFiles)
591
- : allTemplateFiles);
575
+ const files = await resolveInitTemplateFiles(options);
592
576
  const preset = resolvePreset(options.preset);
593
577
  const designSystem = effectiveDesignSystem(options.designSystem);
594
- const created = [];
595
- const skipped = [];
578
+ const result = { targetDir, created: [], skipped: [] };
596
579
  if (!options.dryRun) {
597
580
  await mkdir(targetDir, { recursive: true });
598
581
  }
599
582
  if (options.writeConfig) {
600
- const destination = path.join(targetDir, configFileName);
601
- const destinationExists = await exists(destination);
602
- if (destinationExists && !options.force) {
603
- skipped.push(configFileName);
604
- }
605
- else {
606
- created.push(configFileName);
607
- if (!options.dryRun) {
608
- await writeFile(destination, serializeConfig(getResolvedConfig(options)));
609
- }
610
- }
583
+ await installFileIfAllowed(targetDir, configFileName, options, result, () => serializeConfig(getResolvedConfig(options)));
611
584
  }
612
585
  for (const file of files) {
613
- const destination = path.join(targetDir, file);
614
- const destinationExists = await exists(destination);
615
- if (destinationExists && !options.force) {
616
- skipped.push(file);
617
- continue;
618
- }
619
- created.push(file);
620
- if (!options.dryRun) {
621
- await mkdir(path.dirname(destination), { recursive: true });
586
+ await installFileIfAllowed(targetDir, file, options, result, async () => {
622
587
  const content = await buildInitTemplateContent(file, preset, options.personalization, designSystem);
623
- await writeFile(destination, wrapManagedBlock(file, content));
624
- }
588
+ return wrapManagedBlock(file, content);
589
+ });
625
590
  }
626
591
  if (preset) {
627
- const stackFile = "STACK.md";
628
- const destination = path.join(targetDir, stackFile);
629
- const destinationExists = await exists(destination);
630
- if (destinationExists && !options.force) {
631
- skipped.push(stackFile);
632
- }
633
- else {
634
- created.push(stackFile);
635
- if (!options.dryRun) {
636
- await writeFile(destination, wrapManagedBlock(stackFile, getStackGuidance(preset)));
637
- }
638
- }
592
+ await installFileIfAllowed(targetDir, "STACK.md", options, result, () => wrapManagedBlock("STACK.md", getStackGuidance(preset)));
639
593
  }
640
- return { targetDir, created, skipped };
594
+ return result;
641
595
  }
642
596
  function replaceManagedBlock(file, existingContent, nextContent) {
643
597
  const id = getTemplateId(file);
@@ -758,110 +712,113 @@ function printUpdateResult(result, dryRun = false) {
758
712
  console.log("All managed AgentKit files are current.");
759
713
  }
760
714
  }
761
- async function resolveInteractiveTarget(target, options) {
762
- const providedPreset = resolvePreset(options.preset);
763
- const shouldPrompt = !options.yes && (options.interactive || process.stdin.isTTY);
764
- if (!shouldPrompt) {
715
+ async function promptForTarget(target) {
716
+ if (target && target !== ".") {
765
717
  return target;
766
718
  }
767
- intro("Welcome to AgentKit");
768
- let resolvedTarget = target;
769
- if (!target || target === ".") {
770
- const targetResponse = await text({
771
- message: "Where should AgentKit install files?",
772
- placeholder: ".",
773
- defaultValue: ".",
774
- });
775
- if (isCancel(targetResponse)) {
776
- process.exit(130);
777
- }
778
- resolvedTarget = targetResponse || ".";
779
- }
780
- if (!providedPreset) {
781
- const projectTypeResponse = await select({
782
- message: "What type of project is this?",
783
- initialValue: "generic",
784
- options: [
785
- { label: "Generic TypeScript project", value: "generic" },
786
- { label: "Next.js app", value: "next" },
787
- { label: "SvelteKit app", value: "sveltekit" },
788
- { label: "Express API", value: "express" },
789
- { label: "Convex app", value: "convex" },
790
- { label: "Fullstack app", value: "fullstack" },
791
- ],
792
- });
793
- if (isCancel(projectTypeResponse)) {
794
- process.exit(130);
795
- }
796
- options.preset = resolveProjectPreset(projectTypeResponse);
797
- }
798
- const aiToolResponse = await multiselect({
719
+ return (resolvePrompt(await text({
720
+ message: "Where should AgentKit install files?",
721
+ placeholder: ".",
722
+ defaultValue: ".",
723
+ })) || ".");
724
+ }
725
+ async function promptForProjectPreset() {
726
+ const projectType = resolvePrompt(await select({
727
+ message: "What type of project is this?",
728
+ initialValue: "generic",
729
+ options: [
730
+ { label: "Generic TypeScript project", value: "generic" },
731
+ { label: "Next.js app", value: "next" },
732
+ { label: "SvelteKit app", value: "sveltekit" },
733
+ { label: "Express API", value: "express" },
734
+ { label: "Convex app", value: "convex" },
735
+ { label: "Fullstack app", value: "fullstack" },
736
+ ],
737
+ }));
738
+ return resolveProjectPreset(projectType);
739
+ }
740
+ async function promptForAiTools(defaults) {
741
+ return resolvePrompt(await multiselect({
799
742
  message: "Which AI tools do you use?",
800
- initialValues: options.aiTools ?? ["codex", "cursor", "claude"],
743
+ initialValues: defaults ?? ["codex", "cursor", "claude"],
801
744
  options: [
802
745
  { label: "Codex", value: "codex" },
803
746
  { label: "Cursor", value: "cursor" },
804
747
  { label: "Claude Code", value: "claude" },
805
748
  { label: "GitHub Copilot", value: "copilot" },
806
749
  ],
807
- });
808
- if (isCancel(aiToolResponse)) {
809
- process.exit(130);
810
- }
811
- const templateSetResponse = await select({
750
+ }));
751
+ }
752
+ async function promptForTemplateSet(defaultValue) {
753
+ return resolvePrompt(await select({
812
754
  message: "Which template set do you want?",
813
- initialValue: options.templateSet ?? "standard",
755
+ initialValue: defaultValue ?? "standard",
814
756
  options: [
815
757
  { label: "Minimal", value: "minimal" },
816
758
  { label: "Standard", value: "standard" },
817
759
  { label: "Full", value: "full" },
818
760
  ],
819
- });
820
- if (isCancel(templateSetResponse)) {
821
- process.exit(130);
761
+ }));
762
+ }
763
+ async function promptForDesignSystem(defaultValue) {
764
+ return resolvePrompt(await select({
765
+ message: "Which design system guidance?",
766
+ initialValue: effectiveDesignSystem(defaultValue),
767
+ options: validDesignSystems.map((value) => ({ label: designSystemLabels[value], value })),
768
+ }));
769
+ }
770
+ async function findExistingInstallFiles(target, files) {
771
+ const targetDir = path.resolve(process.cwd(), target || ".");
772
+ const existingFiles = [];
773
+ for (const file of files) {
774
+ if (await exists(path.join(targetDir, file))) {
775
+ existingFiles.push(file);
776
+ }
777
+ }
778
+ return existingFiles;
779
+ }
780
+ async function promptForConflictStrategy(existingFiles) {
781
+ return resolvePrompt(await select({
782
+ message: `Existing files found: ${existingFiles.join(", ")}. How should AgentKit handle conflicts?`,
783
+ initialValue: "skip",
784
+ options: [
785
+ { label: "Skip existing files", value: "skip" },
786
+ { label: "Overwrite existing files", value: "overwrite" },
787
+ ],
788
+ }));
789
+ }
790
+ async function applyInteractiveSelections(resolvedTarget, providedPreset, options) {
791
+ if (!providedPreset) {
792
+ options.preset = await promptForProjectPreset();
822
793
  }
794
+ const aiTools = await promptForAiTools(options.aiTools);
795
+ const templateSet = await promptForTemplateSet(options.templateSet);
823
796
  const templateFiles = await getTemplateFiles();
824
- options.templateSet = templateSetResponse;
825
- options.aiTools = aiToolResponse;
826
- options.files = getSelectedTemplateFiles(templateSetResponse, aiToolResponse, templateFiles);
827
- if (templateSetResponse === "standard" || templateSetResponse === "full") {
828
- const designSystemResponse = await select({
829
- message: "Which design system guidance?",
830
- initialValue: effectiveDesignSystem(options.designSystem),
831
- options: validDesignSystems.map((value) => ({ label: designSystemLabels[value], value })),
832
- });
833
- if (isCancel(designSystemResponse)) {
834
- process.exit(130);
835
- }
836
- options.designSystem = designSystemResponse;
797
+ options.templateSet = templateSet;
798
+ options.aiTools = aiTools;
799
+ options.files = getSelectedTemplateFiles(templateSet, aiTools, templateFiles);
800
+ if (templateSet === "standard" || templateSet === "full") {
801
+ options.designSystem = await promptForDesignSystem(options.designSystem);
837
802
  }
838
803
  if (!options.force) {
839
804
  const preset = resolvePreset(options.preset);
840
805
  const installFiles = preset ? [...options.files, "STACK.md"] : options.files;
841
- const targetDir = path.resolve(process.cwd(), resolvedTarget || ".");
842
- const existingFiles = [];
843
- for (const file of installFiles) {
844
- if (await exists(path.join(targetDir, file))) {
845
- existingFiles.push(file);
846
- }
847
- }
806
+ const existingFiles = await findExistingInstallFiles(resolvedTarget, installFiles);
848
807
  if (existingFiles.length > 0) {
849
- const conflictResponse = await select({
850
- message: `Existing files found: ${existingFiles.join(", ")}. How should AgentKit handle conflicts?`,
851
- initialValue: "skip",
852
- options: [
853
- { label: "Skip existing files", value: "skip" },
854
- { label: "Overwrite existing files", value: "overwrite" },
855
- ],
856
- });
857
- if (isCancel(conflictResponse)) {
858
- process.exit(130);
859
- }
860
- options.force = conflictResponse === "overwrite";
808
+ options.force = (await promptForConflictStrategy(existingFiles)) === "overwrite";
861
809
  }
862
810
  }
811
+ }
812
+ async function resolveInteractiveTarget(target, options) {
813
+ const providedPreset = resolvePreset(options.preset);
814
+ if (!shouldPromptForInit(options, process)) {
815
+ return target;
816
+ }
817
+ intro("Welcome to AgentKit");
818
+ const resolvedTarget = await promptForTarget(target);
819
+ await applyInteractiveSelections(resolvedTarget, providedPreset, options);
863
820
  options.personalization = await promptForPersonalization(options.personalization);
864
- return resolvedTarget || ".";
821
+ return resolvedTarget;
865
822
  }
866
823
  async function main() {
867
824
  if (process.argv.slice(2).includes("--list")) {
@@ -932,7 +889,18 @@ Examples:
932
889
  });
933
890
  await program.parseAsync(process.argv);
934
891
  }
935
- if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
892
+ function resolveCliPath(filePath) {
893
+ try {
894
+ return realpathSync(filePath);
895
+ }
896
+ catch {
897
+ return path.resolve(filePath);
898
+ }
899
+ }
900
+ function isDirectCliInvocation(argvPath) {
901
+ return Boolean(argvPath && resolveCliPath(argvPath) === resolveCliPath(__filename));
902
+ }
903
+ if (isDirectCliInvocation(process.argv[1])) {
936
904
  main().catch((error) => {
937
905
  const message = error instanceof Error ? error.message : String(error);
938
906
  console.error(message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thomas-agentkit",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "description": "Install AI-agent-ready development templates into a project.",
5
5
  "license": "MIT",
6
6
  "repository": {