thomas-agentkit 0.3.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 +52 -6
  2. package/dist/cli.js +509 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,16 +14,26 @@ Install templates into the current directory:
14
14
  npx thomas-agentkit init
15
15
  ```
16
16
 
17
+ By default, `init` opens a short interactive setup flow in terminals. After choosing install options, you can optionally personalize repository-level template placeholders such as project name, description, issue tracker, docs paths, stack summary, and project commands.
18
+
17
19
  Install into another directory:
18
20
 
19
21
  ```bash
20
22
  npx thomas-agentkit init ./my-project
21
23
  ```
22
24
 
25
+ Accept defaults without prompts:
26
+
27
+ ```bash
28
+ npx thomas-agentkit init --yes
29
+ ```
30
+
31
+ `--yes` keeps installed templates generic and leaves placeholders for later editing.
32
+
23
33
  Preview changes without writing files:
24
34
 
25
35
  ```bash
26
- npx thomas-agentkit init --dry-run
36
+ npx thomas-agentkit init --yes --dry-run
27
37
  ```
28
38
 
29
39
  Overwrite existing files:
@@ -44,7 +54,7 @@ Preview updates without writing files:
44
54
  npx thomas-agentkit update --dry-run
45
55
  ```
46
56
 
47
- Use the optional interactive flow:
57
+ Explicitly request the interactive flow:
48
58
 
49
59
  ```bash
50
60
  npx thomas-agentkit init --interactive
@@ -56,6 +66,37 @@ Install stack-specific agent guidance:
56
66
  npx thomas-agentkit init --preset next
57
67
  ```
58
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
+
59
100
  List bundled templates:
60
101
 
61
102
  ```bash
@@ -97,6 +138,10 @@ Generated content
97
138
 
98
139
  `agentkit update` only replaces content inside matching managed blocks. User edits before or after those blocks are preserved. Existing legacy files without managed blocks are reported as unmanaged and left untouched.
99
140
 
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.
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
+
100
145
  ## Presets
101
146
 
102
147
  Presets add stack-specific guidance without scaffolding framework app files.
@@ -110,7 +155,7 @@ Presets add stack-specific guidance without scaffolding framework app files.
110
155
  ## CLI Reference
111
156
 
112
157
  ```text
113
- agentkit init [target] [--force] [--dry-run] [--interactive] [--yes] [--preset <name>]
158
+ agentkit init [target] [--force] [--dry-run] [--interactive] [--yes] [--write-config] [--preset <name>]
114
159
  agentkit update [target] [--dry-run] [--preset <name>]
115
160
  agentkit --list
116
161
  agentkit --list-presets
@@ -122,8 +167,9 @@ Options:
122
167
 
123
168
  - `--force`: overwrite existing files
124
169
  - `--dry-run`: print planned changes without writing files
125
- - `-i, --interactive`: prompt for install options
126
- - `-y, --yes`: accept defaults for non-interactive runs
170
+ - `-i, --interactive`: explicitly prompt for install options
171
+ - `-y, --yes`: accept defaults without prompts
172
+ - `--write-config`: write resolved install defaults to `agentkit.config.json`
127
173
  - `--preset <name>`: install stack-specific guidance (`next`, `sveltekit`, `express`, `convex`, `fullstack`)
128
174
  - `--list`: list bundled template files
129
175
  - `--list-presets`: list available presets
@@ -136,7 +182,7 @@ For `agentkit update`, `--preset <name>` refreshes preset-specific managed conte
136
182
 
137
183
  ```bash
138
184
  npm install
139
- npm run dev -- init ./tmp-demo --dry-run
185
+ npm run dev -- init ./tmp-demo --yes --dry-run
140
186
  npm run build
141
187
  npm test
142
188
  ```
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { confirm, isCancel, select, text } from "@clack/prompts";
2
+ import { confirm, intro, isCancel, multiselect, select, text } from "@clack/prompts";
3
3
  import { Command } from "commander";
4
4
  import { constants as fsConstants } from "node:fs";
5
5
  import { access, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
@@ -11,6 +11,22 @@ const packageRoot = path.resolve(__dirname, "..");
11
11
  const templatesDir = path.join(packageRoot, "templates");
12
12
  const packageJsonPath = path.join(packageRoot, "package.json");
13
13
  const validPresets = ["next", "sveltekit", "express", "convex", "fullstack"];
14
+ const validProjectTypes = ["generic", ...validPresets];
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
+ ];
14
30
  const presetLabels = {
15
31
  next: "Next.js",
16
32
  sveltekit: "SvelteKit",
@@ -18,6 +34,17 @@ const presetLabels = {
18
34
  convex: "Convex",
19
35
  fullstack: "Fullstack",
20
36
  };
37
+ const aiToolFiles = {
38
+ codex: ["AGENTS.md"],
39
+ cursor: [".cursor/rules/agentkit.md"],
40
+ claude: ["CLAUDE.md"],
41
+ copilot: [".github/copilot-instructions.md"],
42
+ };
43
+ const templateSetFiles = {
44
+ minimal: ["AGENTS.md"],
45
+ standard: ["AGENTS.md", "CODE-QUALITY.md", "DESIGN-SYSTEM.md", "WORKFLOWS.md"],
46
+ full: [],
47
+ };
21
48
  const stackGuidance = {
22
49
  next: `# Stack Guidance
23
50
 
@@ -101,9 +128,21 @@ async function getTemplateFiles(dir = templatesDir, base = templatesDir) {
101
128
  function isPresetName(value) {
102
129
  return validPresets.includes(value);
103
130
  }
131
+ function isProjectTypeName(value) {
132
+ return validProjectTypes.includes(value);
133
+ }
134
+ function isAiToolName(value) {
135
+ return validAiTools.includes(value);
136
+ }
137
+ function isTemplateSetName(value) {
138
+ return validTemplateSets.includes(value);
139
+ }
104
140
  function formatPresetList() {
105
141
  return validPresets.join(", ");
106
142
  }
143
+ function formatTemplateSetList() {
144
+ return validTemplateSets.join(", ");
145
+ }
107
146
  function resolvePreset(preset) {
108
147
  if (!preset) {
109
148
  return undefined;
@@ -114,6 +153,147 @@ function resolvePreset(preset) {
114
153
  }
115
154
  return normalizedPreset;
116
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
+ }
265
+ export function resolveProjectPreset(projectType) {
266
+ if (!projectType) {
267
+ return undefined;
268
+ }
269
+ const normalizedProjectType = projectType.toLowerCase();
270
+ if (!isProjectTypeName(normalizedProjectType)) {
271
+ throw new Error(`Unknown project type "${projectType}". Valid project types: ${validProjectTypes.join(", ")}.`);
272
+ }
273
+ return normalizedProjectType === "generic" ? undefined : normalizedProjectType;
274
+ }
275
+ export function getFilesForAiTools(aiTools) {
276
+ const files = new Set();
277
+ for (const aiTool of aiTools) {
278
+ if (!isAiToolName(aiTool)) {
279
+ throw new Error(`Unknown AI tool "${aiTool}". Valid AI tools: ${validAiTools.join(", ")}.`);
280
+ }
281
+ for (const file of aiToolFiles[aiTool]) {
282
+ files.add(file);
283
+ }
284
+ }
285
+ return [...files].sort();
286
+ }
287
+ export function getFilesForTemplateSet(templateSet, allTemplateFiles) {
288
+ if (templateSet === "full") {
289
+ return [...allTemplateFiles].sort();
290
+ }
291
+ return templateSetFiles[templateSet].filter((file) => allTemplateFiles.includes(file)).sort();
292
+ }
293
+ export function getSelectedTemplateFiles(templateSet, aiTools, allTemplateFiles) {
294
+ const files = new Set([...getFilesForTemplateSet(templateSet, allTemplateFiles), ...getFilesForAiTools(aiTools)]);
295
+ return [...files].filter((file) => allTemplateFiles.includes(file)).sort();
296
+ }
117
297
  function getStackGuidance(preset) {
118
298
  return preset === "fullstack" ? fullstackGuidance : stackGuidance[preset];
119
299
  }
@@ -140,17 +320,249 @@ function addStackReference(file, content, preset) {
140
320
  }
141
321
  return `${content.trimEnd()}\n${stackNote}`;
142
322
  }
323
+ function cleanPersonalizationValue(value) {
324
+ const trimmed = value?.trim();
325
+ return trimmed ? trimmed : undefined;
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
+ }
353
+ function replaceIfProvided(content, placeholder, value) {
354
+ const replacement = cleanPersonalizationValue(value);
355
+ return replacement ? content.replaceAll(placeholder, replacement) : content;
356
+ }
357
+ function getProvidedCommands(values) {
358
+ return [values.testCommand, values.lintCommand, values.buildCommand]
359
+ .map(cleanPersonalizationValue)
360
+ .filter((command) => Boolean(command));
361
+ }
362
+ function commandDescription(command, kind) {
363
+ const descriptions = {
364
+ test: "Run tests",
365
+ lint: "Run lint checks",
366
+ build: "Build or check the project",
367
+ };
368
+ return descriptions[kind] ?? command;
369
+ }
370
+ function replaceCommandBlock(content, commands) {
371
+ if (commands.length === 0) {
372
+ return content;
373
+ }
374
+ const commandBlock = ["```bash", ...commands, "```"].join("\n");
375
+ return content
376
+ .replace(/```bash\nnpm install\nnpm test\nnpm run build\nnpm run lint\n```/, commandBlock)
377
+ .replace(/```bash\nnpm test\nnpm run lint\nnpm run build\n```/, commandBlock);
378
+ }
379
+ function replaceAgentCommandTable(content, values) {
380
+ const rows = [
381
+ cleanPersonalizationValue(values.testCommand)
382
+ ? `| \`${cleanPersonalizationValue(values.testCommand)}\` | ${commandDescription(values.testCommand ?? "", "test")} |`
383
+ : undefined,
384
+ cleanPersonalizationValue(values.lintCommand)
385
+ ? `| \`${cleanPersonalizationValue(values.lintCommand)}\` | ${commandDescription(values.lintCommand ?? "", "lint")} |`
386
+ : undefined,
387
+ cleanPersonalizationValue(values.buildCommand)
388
+ ? `| \`${cleanPersonalizationValue(values.buildCommand)}\` | ${commandDescription(values.buildCommand ?? "", "build")} |`
389
+ : undefined,
390
+ ].filter((row) => Boolean(row));
391
+ if (rows.length === 0) {
392
+ return content;
393
+ }
394
+ const nextTable = ["| Command | Description |", "| --- | --- |", ...rows].join("\n");
395
+ return content.replace(/\| Command \| Description \|\n\| --- \| --- \|\n\| `npm run dev` \| Start the local development server \|\n\| `npm test` \| Run tests \|\n\| `npm run lint` \| Run lint checks \|\n\| `npm run build` \| Build the project \|/, nextTable);
396
+ }
397
+ function replaceStackSummary(content, stackSummary) {
398
+ const summary = cleanPersonalizationValue(stackSummary);
399
+ if (!summary) {
400
+ return content;
401
+ }
402
+ const stackItems = summary
403
+ .split(/\r?\n|,/)
404
+ .map((item) => item.trim())
405
+ .filter(Boolean)
406
+ .map((item) => `- ${item}`)
407
+ .join("\n");
408
+ return content.replace(/- \[Primary framework\]\n- \[Language\/runtime\]\n- \[Backend\/data layer\]\n- \[Styling system\]\n- \[Test tools\]\n- \[Lint\/format tools\]/, stackItems);
409
+ }
410
+ export function personalizeTemplateContent(file, content, values) {
411
+ if (!values) {
412
+ return content;
413
+ }
414
+ if (file === "PRD-TEMPLATE.md" ||
415
+ file === "IMPLEMENTATION-BRIEF-TEMPLATE.md" ||
416
+ file === ".github/pull_request_template.md") {
417
+ return content;
418
+ }
419
+ let personalized = content;
420
+ if (file === "AGENTS.md" || file === "DESIGN-SYSTEM.md") {
421
+ personalized = replaceIfProvided(personalized, "[Project Name]", values.projectName);
422
+ }
423
+ if (file === "AGENTS.md") {
424
+ personalized = replaceIfProvided(personalized, "[short project description]", values.projectDescription);
425
+ personalized = replaceIfProvided(personalized, "[issue tracker, e.g. Linear or GitHub Issues]", values.issueTracker);
426
+ personalized = replaceIfProvided(personalized, "[design system path, e.g. docs/design-system.md]", values.designSystemPath);
427
+ personalized = replaceIfProvided(personalized, "[design system path]", values.designSystemPath);
428
+ personalized = replaceIfProvided(personalized, "[briefs path, e.g. docs/briefs]", values.briefsPath);
429
+ personalized = replaceIfProvided(personalized, "[test command, e.g. npm test]", values.testCommand);
430
+ personalized = replaceIfProvided(personalized, "[lint command, e.g. npm run lint]", values.lintCommand);
431
+ personalized = replaceIfProvided(personalized, "[build/check command, e.g. npm run build]", values.buildCommand);
432
+ personalized = replaceAgentCommandTable(personalized, values);
433
+ personalized = replaceStackSummary(personalized, values.stackSummary);
434
+ }
435
+ if (file === "CLAUDE.md" || file === "CODE-QUALITY.md") {
436
+ personalized = replaceCommandBlock(personalized, getProvidedCommands(values));
437
+ }
438
+ return personalized;
439
+ }
440
+ async function promptForPersonalization(defaults) {
441
+ const shouldPersonalize = await confirm({
442
+ message: "Personalize template placeholders?",
443
+ initialValue: Boolean(defaults),
444
+ });
445
+ if (isCancel(shouldPersonalize)) {
446
+ process.exit(130);
447
+ }
448
+ if (!shouldPersonalize) {
449
+ return undefined;
450
+ }
451
+ const projectName = await text({
452
+ message: "Project name",
453
+ placeholder: "[Project Name]",
454
+ defaultValue: defaults?.projectName,
455
+ });
456
+ if (isCancel(projectName)) {
457
+ process.exit(130);
458
+ }
459
+ const projectDescription = await text({
460
+ message: "Short project description",
461
+ placeholder: "[short project description]",
462
+ defaultValue: defaults?.projectDescription,
463
+ });
464
+ if (isCancel(projectDescription)) {
465
+ process.exit(130);
466
+ }
467
+ const issueTracker = await text({
468
+ message: "Issue tracker name",
469
+ placeholder: "Linear or GitHub Issues",
470
+ defaultValue: defaults?.issueTracker,
471
+ });
472
+ if (isCancel(issueTracker)) {
473
+ process.exit(130);
474
+ }
475
+ const designSystemPath = await text({
476
+ message: "Design system path",
477
+ placeholder: "docs/design-system.md",
478
+ defaultValue: defaults?.designSystemPath,
479
+ });
480
+ if (isCancel(designSystemPath)) {
481
+ process.exit(130);
482
+ }
483
+ const briefsPath = await text({
484
+ message: "Briefs path",
485
+ placeholder: "docs/briefs",
486
+ defaultValue: defaults?.briefsPath,
487
+ });
488
+ if (isCancel(briefsPath)) {
489
+ process.exit(130);
490
+ }
491
+ const testCommand = await text({
492
+ message: "Test command",
493
+ placeholder: "npm test",
494
+ defaultValue: defaults?.testCommand,
495
+ });
496
+ if (isCancel(testCommand)) {
497
+ process.exit(130);
498
+ }
499
+ const lintCommand = await text({
500
+ message: "Lint command",
501
+ placeholder: "npm run lint",
502
+ defaultValue: defaults?.lintCommand,
503
+ });
504
+ if (isCancel(lintCommand)) {
505
+ process.exit(130);
506
+ }
507
+ const buildCommand = await text({
508
+ message: "Build/check command",
509
+ placeholder: "npm run build",
510
+ defaultValue: defaults?.buildCommand,
511
+ });
512
+ if (isCancel(buildCommand)) {
513
+ process.exit(130);
514
+ }
515
+ const stackSummary = await text({
516
+ message: "Stack summary",
517
+ placeholder: "Next.js, TypeScript, Tailwind CSS, Vitest",
518
+ defaultValue: defaults?.stackSummary,
519
+ });
520
+ if (isCancel(stackSummary)) {
521
+ process.exit(130);
522
+ }
523
+ return {
524
+ projectName,
525
+ projectDescription,
526
+ issueTracker,
527
+ designSystemPath,
528
+ briefsPath,
529
+ testCommand,
530
+ lintCommand,
531
+ buildCommand,
532
+ stackSummary,
533
+ };
534
+ }
535
+ async function buildInitTemplateContent(file, preset, personalization) {
536
+ const content = await buildTemplateContent(file, preset);
537
+ return personalizeTemplateContent(file, content, personalization);
538
+ }
143
539
  async function installTemplates(targetArg, options) {
144
540
  const targetDir = path.resolve(process.cwd(), targetArg || ".");
145
- const 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);
146
546
  const preset = resolvePreset(options.preset);
147
547
  const created = [];
148
548
  const skipped = [];
149
549
  if (!options.dryRun) {
150
550
  await mkdir(targetDir, { recursive: true });
151
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
+ }
152
565
  for (const file of files) {
153
- const source = path.join(templatesDir, file);
154
566
  const destination = path.join(targetDir, file);
155
567
  const destinationExists = await exists(destination);
156
568
  if (destinationExists && !options.force) {
@@ -161,11 +573,11 @@ async function installTemplates(targetArg, options) {
161
573
  if (!options.dryRun) {
162
574
  await mkdir(path.dirname(destination), { recursive: true });
163
575
  if (preset && file === "AGENTS.md") {
164
- const content = await readFile(source, "utf8");
165
- await writeFile(destination, wrapManagedBlock(file, addStackReference(file, content, preset)));
576
+ const content = await buildInitTemplateContent(file, preset, options.personalization);
577
+ await writeFile(destination, wrapManagedBlock(file, content));
166
578
  }
167
579
  else {
168
- const content = await readFile(source, "utf8");
580
+ const content = await buildInitTemplateContent(file, preset, options.personalization);
169
581
  await writeFile(destination, wrapManagedBlock(file, content));
170
582
  }
171
583
  }
@@ -301,42 +713,97 @@ function printUpdateResult(result, dryRun = false) {
301
713
  }
302
714
  async function resolveInteractiveTarget(target, options) {
303
715
  const providedPreset = resolvePreset(options.preset);
304
- if (!options.interactive) {
716
+ const shouldPrompt = !options.yes && (options.interactive || process.stdin.isTTY);
717
+ if (!shouldPrompt) {
305
718
  return target;
306
719
  }
307
- const targetResponse = await text({
308
- message: "Where should AgentKit install templates?",
309
- placeholder: target || ".",
310
- defaultValue: target || ".",
311
- });
312
- if (isCancel(targetResponse)) {
313
- process.exit(130);
720
+ intro("Welcome to AgentKit");
721
+ let resolvedTarget = target;
722
+ if (!target || target === ".") {
723
+ const targetResponse = await text({
724
+ message: "Where should AgentKit install files?",
725
+ placeholder: ".",
726
+ defaultValue: ".",
727
+ });
728
+ if (isCancel(targetResponse)) {
729
+ process.exit(130);
730
+ }
731
+ resolvedTarget = targetResponse || ".";
732
+ }
733
+ if (!providedPreset) {
734
+ const projectTypeResponse = await select({
735
+ message: "What type of project is this?",
736
+ initialValue: "generic",
737
+ options: [
738
+ { label: "Generic TypeScript project", value: "generic" },
739
+ { label: "Next.js app", value: "next" },
740
+ { label: "SvelteKit app", value: "sveltekit" },
741
+ { label: "Express API", value: "express" },
742
+ { label: "Convex app", value: "convex" },
743
+ { label: "Fullstack app", value: "fullstack" },
744
+ ],
745
+ });
746
+ if (isCancel(projectTypeResponse)) {
747
+ process.exit(130);
748
+ }
749
+ options.preset = resolveProjectPreset(projectTypeResponse);
314
750
  }
315
- const forceResponse = await confirm({
316
- message: "Overwrite existing files?",
317
- initialValue: Boolean(options.force),
751
+ const aiToolResponse = await multiselect({
752
+ message: "Which AI tools do you use?",
753
+ initialValues: options.aiTools ?? ["codex", "cursor", "claude"],
754
+ options: [
755
+ { label: "Codex", value: "codex" },
756
+ { label: "Cursor", value: "cursor" },
757
+ { label: "Claude Code", value: "claude" },
758
+ { label: "GitHub Copilot", value: "copilot" },
759
+ ],
318
760
  });
319
- if (isCancel(forceResponse)) {
761
+ if (isCancel(aiToolResponse)) {
320
762
  process.exit(130);
321
763
  }
322
- const presetResponse = await select({
323
- message: "Which preset should AgentKit use?",
324
- initialValue: providedPreset || "generic",
764
+ const templateSetResponse = await select({
765
+ message: "Which template set do you want?",
766
+ initialValue: options.templateSet ?? "standard",
325
767
  options: [
326
- { label: "Generic", value: "generic" },
327
- { label: "Next.js", value: "next" },
328
- { label: "SvelteKit", value: "sveltekit" },
329
- { label: "Express", value: "express" },
330
- { label: "Convex", value: "convex" },
331
- { label: "Fullstack", value: "fullstack" },
768
+ { label: "Minimal", value: "minimal" },
769
+ { label: "Standard", value: "standard" },
770
+ { label: "Full", value: "full" },
332
771
  ],
333
772
  });
334
- if (isCancel(presetResponse)) {
773
+ if (isCancel(templateSetResponse)) {
335
774
  process.exit(130);
336
775
  }
337
- options.preset = presetResponse === "generic" ? undefined : presetResponse;
338
- options.force = forceResponse;
339
- return targetResponse || ".";
776
+ const templateFiles = await getTemplateFiles();
777
+ options.templateSet = templateSetResponse;
778
+ options.aiTools = aiToolResponse;
779
+ options.files = getSelectedTemplateFiles(templateSetResponse, aiToolResponse, templateFiles);
780
+ if (!options.force) {
781
+ const preset = resolvePreset(options.preset);
782
+ const installFiles = preset ? [...options.files, "STACK.md"] : options.files;
783
+ const targetDir = path.resolve(process.cwd(), resolvedTarget || ".");
784
+ const existingFiles = [];
785
+ for (const file of installFiles) {
786
+ if (await exists(path.join(targetDir, file))) {
787
+ existingFiles.push(file);
788
+ }
789
+ }
790
+ if (existingFiles.length > 0) {
791
+ const conflictResponse = await select({
792
+ message: `Existing files found: ${existingFiles.join(", ")}. How should AgentKit handle conflicts?`,
793
+ initialValue: "skip",
794
+ options: [
795
+ { label: "Skip existing files", value: "skip" },
796
+ { label: "Overwrite existing files", value: "overwrite" },
797
+ ],
798
+ });
799
+ if (isCancel(conflictResponse)) {
800
+ process.exit(130);
801
+ }
802
+ options.force = conflictResponse === "overwrite";
803
+ }
804
+ }
805
+ options.personalization = await promptForPersonalization(options.personalization);
806
+ return resolvedTarget || ".";
340
807
  }
341
808
  async function main() {
342
809
  if (process.argv.slice(2).includes("--list")) {
@@ -365,7 +832,7 @@ Examples:
365
832
  agentkit init
366
833
  agentkit update
367
834
  agentkit init --preset next
368
- agentkit init ./my-project --dry-run
835
+ agentkit init ./my-project --yes --dry-run
369
836
  agentkit --list-presets
370
837
  agentkit --list`);
371
838
  program
@@ -376,8 +843,10 @@ Examples:
376
843
  .option("--dry-run", "print planned changes without writing files")
377
844
  .option("-i, --interactive", "prompt for install options")
378
845
  .option("-y, --yes", "accept defaults for non-interactive runs")
846
+ .option("--write-config", "write resolved install defaults to agentkit.config.json")
379
847
  .option("--preset <name>", `install stack-specific guidance (${formatPresetList()})`)
380
848
  .action(async (target, options) => {
849
+ await applyInitConfig(options, await loadConfigForTarget(target));
381
850
  const resolvedTarget = await resolveInteractiveTarget(target, options);
382
851
  const result = await installTemplates(resolvedTarget, options);
383
852
  printInstallResult(result, Boolean(options.dryRun));
@@ -389,13 +858,16 @@ Examples:
389
858
  .option("--dry-run", "print planned changes without writing files")
390
859
  .option("--preset <name>", `update stack-specific guidance (${formatPresetList()})`)
391
860
  .action(async (target, options) => {
861
+ applyUpdateConfig(options, await loadConfigForTarget(target));
392
862
  const result = await updateTemplates(target, options);
393
863
  printUpdateResult(result, Boolean(options.dryRun));
394
864
  });
395
865
  await program.parseAsync(process.argv);
396
866
  }
397
- main().catch((error) => {
398
- const message = error instanceof Error ? error.message : String(error);
399
- console.error(message);
400
- process.exitCode = 1;
401
- });
867
+ if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
868
+ main().catch((error) => {
869
+ const message = error instanceof Error ? error.message : String(error);
870
+ console.error(message);
871
+ process.exitCode = 1;
872
+ });
873
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thomas-agentkit",
3
- "version": "0.3.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": {