thomas-agentkit 0.4.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +35 -1
  2. package/dist/cli.js +192 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,6 +66,37 @@ Install stack-specific agent guidance:
66
66
  npx thomas-agentkit init --preset next
67
67
  ```
68
68
 
69
+ Standardize install defaults with `agentkit.config.json`:
70
+
71
+ ```json
72
+ {
73
+ "preset": "next",
74
+ "templateSet": "standard",
75
+ "aiTools": ["codex", "cursor", "claude"],
76
+ "personalization": {
77
+ "projectName": "Acme CRM",
78
+ "projectDescription": "a customer operations dashboard",
79
+ "issueTracker": "Linear",
80
+ "designSystemPath": "docs/design-system.md",
81
+ "briefsPath": "docs/briefs",
82
+ "testCommand": "pnpm test",
83
+ "lintCommand": "pnpm lint",
84
+ "buildCommand": "pnpm build",
85
+ "stackSummary": "Next.js, TypeScript, PostgreSQL"
86
+ }
87
+ }
88
+ ```
89
+
90
+ AgentKit reads `agentkit.config.json` from the target directory first. When installing into another target that does not have a config file, it falls back to the current working directory. Config values are defaults: explicit CLI flags override them.
91
+
92
+ Create a config file from resolved install choices:
93
+
94
+ ```bash
95
+ npx thomas-agentkit init --write-config
96
+ ```
97
+
98
+ `--write-config` writes `agentkit.config.json` in the target directory. Existing config files are skipped by default; use `--force` to overwrite one intentionally.
99
+
69
100
  List bundled templates:
70
101
 
71
102
  ```bash
@@ -109,6 +140,8 @@ Generated content
109
140
 
110
141
  Interactive personalization only applies during `agentkit init` when files are created or overwritten. It does not write a config file, and `agentkit update` does not reapply personalized values.
111
142
 
143
+ `agentkit.config.json` can set `preset`, `templateSet`, `aiTools`, and `personalization` defaults. `templateSet` may be `minimal`, `standard`, or `full`; `aiTools` may include `codex`, `cursor`, `claude`, and `copilot`. `agentkit update` only uses config `preset` and continues to update all managed bundled templates.
144
+
112
145
  ## Presets
113
146
 
114
147
  Presets add stack-specific guidance without scaffolding framework app files.
@@ -122,7 +155,7 @@ Presets add stack-specific guidance without scaffolding framework app files.
122
155
  ## CLI Reference
123
156
 
124
157
  ```text
125
- agentkit init [target] [--force] [--dry-run] [--interactive] [--yes] [--preset <name>]
158
+ agentkit init [target] [--force] [--dry-run] [--interactive] [--yes] [--write-config] [--preset <name>]
126
159
  agentkit update [target] [--dry-run] [--preset <name>]
127
160
  agentkit --list
128
161
  agentkit --list-presets
@@ -136,6 +169,7 @@ Options:
136
169
  - `--dry-run`: print planned changes without writing files
137
170
  - `-i, --interactive`: explicitly prompt for install options
138
171
  - `-y, --yes`: accept defaults without prompts
172
+ - `--write-config`: write resolved install defaults to `agentkit.config.json`
139
173
  - `--preset <name>`: install stack-specific guidance (`next`, `sveltekit`, `express`, `convex`, `fullstack`)
140
174
  - `--list`: list bundled template files
141
175
  - `--list-presets`: list available presets
package/dist/cli.js CHANGED
@@ -13,6 +13,20 @@ const packageJsonPath = path.join(packageRoot, "package.json");
13
13
  const validPresets = ["next", "sveltekit", "express", "convex", "fullstack"];
14
14
  const validProjectTypes = ["generic", ...validPresets];
15
15
  const validAiTools = ["codex", "cursor", "claude", "copilot"];
16
+ const validTemplateSets = ["minimal", "standard", "full"];
17
+ const configFileName = "agentkit.config.json";
18
+ const configKeys = ["preset", "templateSet", "aiTools", "personalization"];
19
+ const personalizationKeys = [
20
+ "projectName",
21
+ "projectDescription",
22
+ "issueTracker",
23
+ "designSystemPath",
24
+ "briefsPath",
25
+ "testCommand",
26
+ "lintCommand",
27
+ "buildCommand",
28
+ "stackSummary",
29
+ ];
16
30
  const presetLabels = {
17
31
  next: "Next.js",
18
32
  sveltekit: "SvelteKit",
@@ -120,9 +134,15 @@ function isProjectTypeName(value) {
120
134
  function isAiToolName(value) {
121
135
  return validAiTools.includes(value);
122
136
  }
137
+ function isTemplateSetName(value) {
138
+ return validTemplateSets.includes(value);
139
+ }
123
140
  function formatPresetList() {
124
141
  return validPresets.join(", ");
125
142
  }
143
+ function formatTemplateSetList() {
144
+ return validTemplateSets.join(", ");
145
+ }
126
146
  function resolvePreset(preset) {
127
147
  if (!preset) {
128
148
  return undefined;
@@ -133,6 +153,115 @@ function resolvePreset(preset) {
133
153
  }
134
154
  return normalizedPreset;
135
155
  }
156
+ function resolveTemplateSet(templateSet) {
157
+ if (!templateSet) {
158
+ return undefined;
159
+ }
160
+ const normalizedTemplateSet = templateSet.toLowerCase();
161
+ if (!isTemplateSetName(normalizedTemplateSet)) {
162
+ throw new Error(`Unknown template set "${templateSet}". Valid template sets: ${formatTemplateSetList()}.`);
163
+ }
164
+ return normalizedTemplateSet;
165
+ }
166
+ function assertPlainObject(value, name) {
167
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
168
+ throw new Error(`${name} must be an object.`);
169
+ }
170
+ }
171
+ function assertKnownKeys(value, validKeys, name) {
172
+ for (const key of Object.keys(value)) {
173
+ if (!validKeys.includes(key)) {
174
+ throw new Error(`Unknown ${name} key "${key}". Valid keys: ${validKeys.join(", ")}.`);
175
+ }
176
+ }
177
+ }
178
+ function parseConfig(rawConfig, configPath) {
179
+ assertPlainObject(rawConfig, configFileName);
180
+ assertKnownKeys(rawConfig, configKeys, configFileName);
181
+ const config = {};
182
+ if (rawConfig.preset !== undefined) {
183
+ if (typeof rawConfig.preset !== "string") {
184
+ throw new Error(`${configFileName} preset must be a string.`);
185
+ }
186
+ config.preset = resolvePreset(rawConfig.preset);
187
+ }
188
+ if (rawConfig.templateSet !== undefined) {
189
+ if (typeof rawConfig.templateSet !== "string") {
190
+ throw new Error(`${configFileName} templateSet must be a string.`);
191
+ }
192
+ config.templateSet = resolveTemplateSet(rawConfig.templateSet);
193
+ }
194
+ if (rawConfig.aiTools !== undefined) {
195
+ if (!Array.isArray(rawConfig.aiTools)) {
196
+ throw new Error(`${configFileName} aiTools must be an array.`);
197
+ }
198
+ config.aiTools = rawConfig.aiTools.map((aiTool) => {
199
+ if (typeof aiTool !== "string" || !isAiToolName(aiTool)) {
200
+ throw new Error(`Unknown AI tool "${String(aiTool)}". Valid AI tools: ${validAiTools.join(", ")}.`);
201
+ }
202
+ return aiTool;
203
+ });
204
+ }
205
+ if (rawConfig.personalization !== undefined) {
206
+ assertPlainObject(rawConfig.personalization, `${configFileName} personalization`);
207
+ assertKnownKeys(rawConfig.personalization, personalizationKeys, `${configFileName} personalization`);
208
+ config.personalization = {};
209
+ for (const [key, value] of Object.entries(rawConfig.personalization)) {
210
+ if (typeof value !== "string") {
211
+ throw new Error(`${configFileName} personalization.${key} must be a string.`);
212
+ }
213
+ config.personalization[key] = value;
214
+ }
215
+ }
216
+ if (config.preset === undefined && rawConfig.preset !== undefined) {
217
+ throw new Error(`Invalid preset in ${configPath}.`);
218
+ }
219
+ return config;
220
+ }
221
+ async function readConfig(configPath) {
222
+ let rawContent;
223
+ try {
224
+ rawContent = await readFile(configPath, "utf8");
225
+ }
226
+ catch {
227
+ throw new Error(`Unable to read ${configPath}.`);
228
+ }
229
+ try {
230
+ return parseConfig(JSON.parse(rawContent), configPath);
231
+ }
232
+ catch (error) {
233
+ const message = error instanceof Error ? error.message : String(error);
234
+ throw new Error(`Invalid ${configPath}: ${message}`);
235
+ }
236
+ }
237
+ async function loadConfigForTarget(targetArg) {
238
+ const cwd = process.cwd();
239
+ const targetDir = path.resolve(cwd, targetArg || ".");
240
+ const targetConfigPath = path.join(targetDir, configFileName);
241
+ if (await exists(targetConfigPath)) {
242
+ return readConfig(targetConfigPath);
243
+ }
244
+ const cwdConfigPath = path.join(cwd, configFileName);
245
+ if (targetDir !== cwd && (await exists(cwdConfigPath))) {
246
+ return readConfig(cwdConfigPath);
247
+ }
248
+ return undefined;
249
+ }
250
+ async function applyInitConfig(options, config) {
251
+ if (!config) {
252
+ return;
253
+ }
254
+ options.preset ??= config.preset;
255
+ options.personalization ??= config.personalization;
256
+ options.templateSet ??= config.templateSet;
257
+ options.aiTools ??= config.aiTools;
258
+ }
259
+ function applyUpdateConfig(options, config) {
260
+ if (!config) {
261
+ return;
262
+ }
263
+ options.preset ??= config.preset;
264
+ }
136
265
  export function resolveProjectPreset(projectType) {
137
266
  if (!projectType) {
138
267
  return undefined;
@@ -195,6 +324,32 @@ function cleanPersonalizationValue(value) {
195
324
  const trimmed = value?.trim();
196
325
  return trimmed ? trimmed : undefined;
197
326
  }
327
+ function getResolvedConfig(options) {
328
+ const config = {
329
+ templateSet: options.templateSet ?? "full",
330
+ aiTools: options.aiTools ?? [],
331
+ };
332
+ const preset = resolvePreset(options.preset);
333
+ if (preset) {
334
+ config.preset = preset;
335
+ }
336
+ if (options.personalization) {
337
+ const personalization = {};
338
+ for (const key of personalizationKeys) {
339
+ const value = cleanPersonalizationValue(options.personalization[key]);
340
+ if (value) {
341
+ personalization[key] = value;
342
+ }
343
+ }
344
+ if (Object.keys(personalization).length > 0) {
345
+ config.personalization = personalization;
346
+ }
347
+ }
348
+ return config;
349
+ }
350
+ function serializeConfig(config) {
351
+ return `${JSON.stringify(config, null, 2)}\n`;
352
+ }
198
353
  function replaceIfProvided(content, placeholder, value) {
199
354
  const replacement = cleanPersonalizationValue(value);
200
355
  return replacement ? content.replaceAll(placeholder, replacement) : content;
@@ -282,10 +437,10 @@ export function personalizeTemplateContent(file, content, values) {
282
437
  }
283
438
  return personalized;
284
439
  }
285
- async function promptForPersonalization() {
440
+ async function promptForPersonalization(defaults) {
286
441
  const shouldPersonalize = await confirm({
287
442
  message: "Personalize template placeholders?",
288
- initialValue: false,
443
+ initialValue: Boolean(defaults),
289
444
  });
290
445
  if (isCancel(shouldPersonalize)) {
291
446
  process.exit(130);
@@ -296,6 +451,7 @@ async function promptForPersonalization() {
296
451
  const projectName = await text({
297
452
  message: "Project name",
298
453
  placeholder: "[Project Name]",
454
+ defaultValue: defaults?.projectName,
299
455
  });
300
456
  if (isCancel(projectName)) {
301
457
  process.exit(130);
@@ -303,6 +459,7 @@ async function promptForPersonalization() {
303
459
  const projectDescription = await text({
304
460
  message: "Short project description",
305
461
  placeholder: "[short project description]",
462
+ defaultValue: defaults?.projectDescription,
306
463
  });
307
464
  if (isCancel(projectDescription)) {
308
465
  process.exit(130);
@@ -310,6 +467,7 @@ async function promptForPersonalization() {
310
467
  const issueTracker = await text({
311
468
  message: "Issue tracker name",
312
469
  placeholder: "Linear or GitHub Issues",
470
+ defaultValue: defaults?.issueTracker,
313
471
  });
314
472
  if (isCancel(issueTracker)) {
315
473
  process.exit(130);
@@ -317,6 +475,7 @@ async function promptForPersonalization() {
317
475
  const designSystemPath = await text({
318
476
  message: "Design system path",
319
477
  placeholder: "docs/design-system.md",
478
+ defaultValue: defaults?.designSystemPath,
320
479
  });
321
480
  if (isCancel(designSystemPath)) {
322
481
  process.exit(130);
@@ -324,6 +483,7 @@ async function promptForPersonalization() {
324
483
  const briefsPath = await text({
325
484
  message: "Briefs path",
326
485
  placeholder: "docs/briefs",
486
+ defaultValue: defaults?.briefsPath,
327
487
  });
328
488
  if (isCancel(briefsPath)) {
329
489
  process.exit(130);
@@ -331,6 +491,7 @@ async function promptForPersonalization() {
331
491
  const testCommand = await text({
332
492
  message: "Test command",
333
493
  placeholder: "npm test",
494
+ defaultValue: defaults?.testCommand,
334
495
  });
335
496
  if (isCancel(testCommand)) {
336
497
  process.exit(130);
@@ -338,6 +499,7 @@ async function promptForPersonalization() {
338
499
  const lintCommand = await text({
339
500
  message: "Lint command",
340
501
  placeholder: "npm run lint",
502
+ defaultValue: defaults?.lintCommand,
341
503
  });
342
504
  if (isCancel(lintCommand)) {
343
505
  process.exit(130);
@@ -345,6 +507,7 @@ async function promptForPersonalization() {
345
507
  const buildCommand = await text({
346
508
  message: "Build/check command",
347
509
  placeholder: "npm run build",
510
+ defaultValue: defaults?.buildCommand,
348
511
  });
349
512
  if (isCancel(buildCommand)) {
350
513
  process.exit(130);
@@ -352,6 +515,7 @@ async function promptForPersonalization() {
352
515
  const stackSummary = await text({
353
516
  message: "Stack summary",
354
517
  placeholder: "Next.js, TypeScript, Tailwind CSS, Vitest",
518
+ defaultValue: defaults?.stackSummary,
355
519
  });
356
520
  if (isCancel(stackSummary)) {
357
521
  process.exit(130);
@@ -374,13 +538,30 @@ async function buildInitTemplateContent(file, preset, personalization) {
374
538
  }
375
539
  async function installTemplates(targetArg, options) {
376
540
  const targetDir = path.resolve(process.cwd(), targetArg || ".");
377
- const files = options.files ?? (await getTemplateFiles());
541
+ const allTemplateFiles = await getTemplateFiles();
542
+ const files = options.files ??
543
+ (options.templateSet || options.aiTools
544
+ ? getSelectedTemplateFiles(options.templateSet ?? "full", options.aiTools ?? [], allTemplateFiles)
545
+ : allTemplateFiles);
378
546
  const preset = resolvePreset(options.preset);
379
547
  const created = [];
380
548
  const skipped = [];
381
549
  if (!options.dryRun) {
382
550
  await mkdir(targetDir, { recursive: true });
383
551
  }
552
+ if (options.writeConfig) {
553
+ const destination = path.join(targetDir, configFileName);
554
+ const destinationExists = await exists(destination);
555
+ if (destinationExists && !options.force) {
556
+ skipped.push(configFileName);
557
+ }
558
+ else {
559
+ created.push(configFileName);
560
+ if (!options.dryRun) {
561
+ await writeFile(destination, serializeConfig(getResolvedConfig(options)));
562
+ }
563
+ }
564
+ }
384
565
  for (const file of files) {
385
566
  const destination = path.join(targetDir, file);
386
567
  const destinationExists = await exists(destination);
@@ -569,7 +750,7 @@ async function resolveInteractiveTarget(target, options) {
569
750
  }
570
751
  const aiToolResponse = await multiselect({
571
752
  message: "Which AI tools do you use?",
572
- initialValues: ["codex", "cursor", "claude"],
753
+ initialValues: options.aiTools ?? ["codex", "cursor", "claude"],
573
754
  options: [
574
755
  { label: "Codex", value: "codex" },
575
756
  { label: "Cursor", value: "cursor" },
@@ -582,7 +763,7 @@ async function resolveInteractiveTarget(target, options) {
582
763
  }
583
764
  const templateSetResponse = await select({
584
765
  message: "Which template set do you want?",
585
- initialValue: "standard",
766
+ initialValue: options.templateSet ?? "standard",
586
767
  options: [
587
768
  { label: "Minimal", value: "minimal" },
588
769
  { label: "Standard", value: "standard" },
@@ -593,6 +774,8 @@ async function resolveInteractiveTarget(target, options) {
593
774
  process.exit(130);
594
775
  }
595
776
  const templateFiles = await getTemplateFiles();
777
+ options.templateSet = templateSetResponse;
778
+ options.aiTools = aiToolResponse;
596
779
  options.files = getSelectedTemplateFiles(templateSetResponse, aiToolResponse, templateFiles);
597
780
  if (!options.force) {
598
781
  const preset = resolvePreset(options.preset);
@@ -619,7 +802,7 @@ async function resolveInteractiveTarget(target, options) {
619
802
  options.force = conflictResponse === "overwrite";
620
803
  }
621
804
  }
622
- options.personalization = await promptForPersonalization();
805
+ options.personalization = await promptForPersonalization(options.personalization);
623
806
  return resolvedTarget || ".";
624
807
  }
625
808
  async function main() {
@@ -660,8 +843,10 @@ Examples:
660
843
  .option("--dry-run", "print planned changes without writing files")
661
844
  .option("-i, --interactive", "prompt for install options")
662
845
  .option("-y, --yes", "accept defaults for non-interactive runs")
846
+ .option("--write-config", "write resolved install defaults to agentkit.config.json")
663
847
  .option("--preset <name>", `install stack-specific guidance (${formatPresetList()})`)
664
848
  .action(async (target, options) => {
849
+ await applyInitConfig(options, await loadConfigForTarget(target));
665
850
  const resolvedTarget = await resolveInteractiveTarget(target, options);
666
851
  const result = await installTemplates(resolvedTarget, options);
667
852
  printInstallResult(result, Boolean(options.dryRun));
@@ -673,6 +858,7 @@ Examples:
673
858
  .option("--dry-run", "print planned changes without writing files")
674
859
  .option("--preset <name>", `update stack-specific guidance (${formatPresetList()})`)
675
860
  .action(async (target, options) => {
861
+ applyUpdateConfig(options, await loadConfigForTarget(target));
676
862
  const result = await updateTemplates(target, options);
677
863
  printUpdateResult(result, Boolean(options.dryRun));
678
864
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thomas-agentkit",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Install AI-agent-ready development templates into a project.",
5
5
  "license": "MIT",
6
6
  "repository": {