thomas-agentkit 0.3.0 → 0.4.0

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 +17 -5
  2. package/dist/cli.js +323 -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
@@ -97,6 +107,8 @@ Generated content
97
107
 
98
108
  `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
109
 
110
+ 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
+
100
112
  ## Presets
101
113
 
102
114
  Presets add stack-specific guidance without scaffolding framework app files.
@@ -122,8 +134,8 @@ Options:
122
134
 
123
135
  - `--force`: overwrite existing files
124
136
  - `--dry-run`: print planned changes without writing files
125
- - `-i, --interactive`: prompt for install options
126
- - `-y, --yes`: accept defaults for non-interactive runs
137
+ - `-i, --interactive`: explicitly prompt for install options
138
+ - `-y, --yes`: accept defaults without prompts
127
139
  - `--preset <name>`: install stack-specific guidance (`next`, `sveltekit`, `express`, `convex`, `fullstack`)
128
140
  - `--list`: list bundled template files
129
141
  - `--list-presets`: list available presets
@@ -136,7 +148,7 @@ For `agentkit update`, `--preset <name>` refreshes preset-specific managed conte
136
148
 
137
149
  ```bash
138
150
  npm install
139
- npm run dev -- init ./tmp-demo --dry-run
151
+ npm run dev -- init ./tmp-demo --yes --dry-run
140
152
  npm run build
141
153
  npm test
142
154
  ```
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,8 @@ 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"];
14
16
  const presetLabels = {
15
17
  next: "Next.js",
16
18
  sveltekit: "SvelteKit",
@@ -18,6 +20,17 @@ const presetLabels = {
18
20
  convex: "Convex",
19
21
  fullstack: "Fullstack",
20
22
  };
23
+ const aiToolFiles = {
24
+ codex: ["AGENTS.md"],
25
+ cursor: [".cursor/rules/agentkit.md"],
26
+ claude: ["CLAUDE.md"],
27
+ copilot: [".github/copilot-instructions.md"],
28
+ };
29
+ const templateSetFiles = {
30
+ minimal: ["AGENTS.md"],
31
+ standard: ["AGENTS.md", "CODE-QUALITY.md", "DESIGN-SYSTEM.md", "WORKFLOWS.md"],
32
+ full: [],
33
+ };
21
34
  const stackGuidance = {
22
35
  next: `# Stack Guidance
23
36
 
@@ -101,6 +114,12 @@ async function getTemplateFiles(dir = templatesDir, base = templatesDir) {
101
114
  function isPresetName(value) {
102
115
  return validPresets.includes(value);
103
116
  }
117
+ function isProjectTypeName(value) {
118
+ return validProjectTypes.includes(value);
119
+ }
120
+ function isAiToolName(value) {
121
+ return validAiTools.includes(value);
122
+ }
104
123
  function formatPresetList() {
105
124
  return validPresets.join(", ");
106
125
  }
@@ -114,6 +133,38 @@ function resolvePreset(preset) {
114
133
  }
115
134
  return normalizedPreset;
116
135
  }
136
+ export function resolveProjectPreset(projectType) {
137
+ if (!projectType) {
138
+ return undefined;
139
+ }
140
+ const normalizedProjectType = projectType.toLowerCase();
141
+ if (!isProjectTypeName(normalizedProjectType)) {
142
+ throw new Error(`Unknown project type "${projectType}". Valid project types: ${validProjectTypes.join(", ")}.`);
143
+ }
144
+ return normalizedProjectType === "generic" ? undefined : normalizedProjectType;
145
+ }
146
+ export function getFilesForAiTools(aiTools) {
147
+ const files = new Set();
148
+ for (const aiTool of aiTools) {
149
+ if (!isAiToolName(aiTool)) {
150
+ throw new Error(`Unknown AI tool "${aiTool}". Valid AI tools: ${validAiTools.join(", ")}.`);
151
+ }
152
+ for (const file of aiToolFiles[aiTool]) {
153
+ files.add(file);
154
+ }
155
+ }
156
+ return [...files].sort();
157
+ }
158
+ export function getFilesForTemplateSet(templateSet, allTemplateFiles) {
159
+ if (templateSet === "full") {
160
+ return [...allTemplateFiles].sort();
161
+ }
162
+ return templateSetFiles[templateSet].filter((file) => allTemplateFiles.includes(file)).sort();
163
+ }
164
+ export function getSelectedTemplateFiles(templateSet, aiTools, allTemplateFiles) {
165
+ const files = new Set([...getFilesForTemplateSet(templateSet, allTemplateFiles), ...getFilesForAiTools(aiTools)]);
166
+ return [...files].filter((file) => allTemplateFiles.includes(file)).sort();
167
+ }
117
168
  function getStackGuidance(preset) {
118
169
  return preset === "fullstack" ? fullstackGuidance : stackGuidance[preset];
119
170
  }
@@ -140,9 +191,190 @@ function addStackReference(file, content, preset) {
140
191
  }
141
192
  return `${content.trimEnd()}\n${stackNote}`;
142
193
  }
194
+ function cleanPersonalizationValue(value) {
195
+ const trimmed = value?.trim();
196
+ return trimmed ? trimmed : undefined;
197
+ }
198
+ function replaceIfProvided(content, placeholder, value) {
199
+ const replacement = cleanPersonalizationValue(value);
200
+ return replacement ? content.replaceAll(placeholder, replacement) : content;
201
+ }
202
+ function getProvidedCommands(values) {
203
+ return [values.testCommand, values.lintCommand, values.buildCommand]
204
+ .map(cleanPersonalizationValue)
205
+ .filter((command) => Boolean(command));
206
+ }
207
+ function commandDescription(command, kind) {
208
+ const descriptions = {
209
+ test: "Run tests",
210
+ lint: "Run lint checks",
211
+ build: "Build or check the project",
212
+ };
213
+ return descriptions[kind] ?? command;
214
+ }
215
+ function replaceCommandBlock(content, commands) {
216
+ if (commands.length === 0) {
217
+ return content;
218
+ }
219
+ const commandBlock = ["```bash", ...commands, "```"].join("\n");
220
+ return content
221
+ .replace(/```bash\nnpm install\nnpm test\nnpm run build\nnpm run lint\n```/, commandBlock)
222
+ .replace(/```bash\nnpm test\nnpm run lint\nnpm run build\n```/, commandBlock);
223
+ }
224
+ function replaceAgentCommandTable(content, values) {
225
+ const rows = [
226
+ cleanPersonalizationValue(values.testCommand)
227
+ ? `| \`${cleanPersonalizationValue(values.testCommand)}\` | ${commandDescription(values.testCommand ?? "", "test")} |`
228
+ : undefined,
229
+ cleanPersonalizationValue(values.lintCommand)
230
+ ? `| \`${cleanPersonalizationValue(values.lintCommand)}\` | ${commandDescription(values.lintCommand ?? "", "lint")} |`
231
+ : undefined,
232
+ cleanPersonalizationValue(values.buildCommand)
233
+ ? `| \`${cleanPersonalizationValue(values.buildCommand)}\` | ${commandDescription(values.buildCommand ?? "", "build")} |`
234
+ : undefined,
235
+ ].filter((row) => Boolean(row));
236
+ if (rows.length === 0) {
237
+ return content;
238
+ }
239
+ const nextTable = ["| Command | Description |", "| --- | --- |", ...rows].join("\n");
240
+ 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);
241
+ }
242
+ function replaceStackSummary(content, stackSummary) {
243
+ const summary = cleanPersonalizationValue(stackSummary);
244
+ if (!summary) {
245
+ return content;
246
+ }
247
+ const stackItems = summary
248
+ .split(/\r?\n|,/)
249
+ .map((item) => item.trim())
250
+ .filter(Boolean)
251
+ .map((item) => `- ${item}`)
252
+ .join("\n");
253
+ return content.replace(/- \[Primary framework\]\n- \[Language\/runtime\]\n- \[Backend\/data layer\]\n- \[Styling system\]\n- \[Test tools\]\n- \[Lint\/format tools\]/, stackItems);
254
+ }
255
+ export function personalizeTemplateContent(file, content, values) {
256
+ if (!values) {
257
+ return content;
258
+ }
259
+ if (file === "PRD-TEMPLATE.md" ||
260
+ file === "IMPLEMENTATION-BRIEF-TEMPLATE.md" ||
261
+ file === ".github/pull_request_template.md") {
262
+ return content;
263
+ }
264
+ let personalized = content;
265
+ if (file === "AGENTS.md" || file === "DESIGN-SYSTEM.md") {
266
+ personalized = replaceIfProvided(personalized, "[Project Name]", values.projectName);
267
+ }
268
+ if (file === "AGENTS.md") {
269
+ personalized = replaceIfProvided(personalized, "[short project description]", values.projectDescription);
270
+ personalized = replaceIfProvided(personalized, "[issue tracker, e.g. Linear or GitHub Issues]", values.issueTracker);
271
+ personalized = replaceIfProvided(personalized, "[design system path, e.g. docs/design-system.md]", values.designSystemPath);
272
+ personalized = replaceIfProvided(personalized, "[design system path]", values.designSystemPath);
273
+ personalized = replaceIfProvided(personalized, "[briefs path, e.g. docs/briefs]", values.briefsPath);
274
+ personalized = replaceIfProvided(personalized, "[test command, e.g. npm test]", values.testCommand);
275
+ personalized = replaceIfProvided(personalized, "[lint command, e.g. npm run lint]", values.lintCommand);
276
+ personalized = replaceIfProvided(personalized, "[build/check command, e.g. npm run build]", values.buildCommand);
277
+ personalized = replaceAgentCommandTable(personalized, values);
278
+ personalized = replaceStackSummary(personalized, values.stackSummary);
279
+ }
280
+ if (file === "CLAUDE.md" || file === "CODE-QUALITY.md") {
281
+ personalized = replaceCommandBlock(personalized, getProvidedCommands(values));
282
+ }
283
+ return personalized;
284
+ }
285
+ async function promptForPersonalization() {
286
+ const shouldPersonalize = await confirm({
287
+ message: "Personalize template placeholders?",
288
+ initialValue: false,
289
+ });
290
+ if (isCancel(shouldPersonalize)) {
291
+ process.exit(130);
292
+ }
293
+ if (!shouldPersonalize) {
294
+ return undefined;
295
+ }
296
+ const projectName = await text({
297
+ message: "Project name",
298
+ placeholder: "[Project Name]",
299
+ });
300
+ if (isCancel(projectName)) {
301
+ process.exit(130);
302
+ }
303
+ const projectDescription = await text({
304
+ message: "Short project description",
305
+ placeholder: "[short project description]",
306
+ });
307
+ if (isCancel(projectDescription)) {
308
+ process.exit(130);
309
+ }
310
+ const issueTracker = await text({
311
+ message: "Issue tracker name",
312
+ placeholder: "Linear or GitHub Issues",
313
+ });
314
+ if (isCancel(issueTracker)) {
315
+ process.exit(130);
316
+ }
317
+ const designSystemPath = await text({
318
+ message: "Design system path",
319
+ placeholder: "docs/design-system.md",
320
+ });
321
+ if (isCancel(designSystemPath)) {
322
+ process.exit(130);
323
+ }
324
+ const briefsPath = await text({
325
+ message: "Briefs path",
326
+ placeholder: "docs/briefs",
327
+ });
328
+ if (isCancel(briefsPath)) {
329
+ process.exit(130);
330
+ }
331
+ const testCommand = await text({
332
+ message: "Test command",
333
+ placeholder: "npm test",
334
+ });
335
+ if (isCancel(testCommand)) {
336
+ process.exit(130);
337
+ }
338
+ const lintCommand = await text({
339
+ message: "Lint command",
340
+ placeholder: "npm run lint",
341
+ });
342
+ if (isCancel(lintCommand)) {
343
+ process.exit(130);
344
+ }
345
+ const buildCommand = await text({
346
+ message: "Build/check command",
347
+ placeholder: "npm run build",
348
+ });
349
+ if (isCancel(buildCommand)) {
350
+ process.exit(130);
351
+ }
352
+ const stackSummary = await text({
353
+ message: "Stack summary",
354
+ placeholder: "Next.js, TypeScript, Tailwind CSS, Vitest",
355
+ });
356
+ if (isCancel(stackSummary)) {
357
+ process.exit(130);
358
+ }
359
+ return {
360
+ projectName,
361
+ projectDescription,
362
+ issueTracker,
363
+ designSystemPath,
364
+ briefsPath,
365
+ testCommand,
366
+ lintCommand,
367
+ buildCommand,
368
+ stackSummary,
369
+ };
370
+ }
371
+ async function buildInitTemplateContent(file, preset, personalization) {
372
+ const content = await buildTemplateContent(file, preset);
373
+ return personalizeTemplateContent(file, content, personalization);
374
+ }
143
375
  async function installTemplates(targetArg, options) {
144
376
  const targetDir = path.resolve(process.cwd(), targetArg || ".");
145
- const files = await getTemplateFiles();
377
+ const files = options.files ?? (await getTemplateFiles());
146
378
  const preset = resolvePreset(options.preset);
147
379
  const created = [];
148
380
  const skipped = [];
@@ -150,7 +382,6 @@ async function installTemplates(targetArg, options) {
150
382
  await mkdir(targetDir, { recursive: true });
151
383
  }
152
384
  for (const file of files) {
153
- const source = path.join(templatesDir, file);
154
385
  const destination = path.join(targetDir, file);
155
386
  const destinationExists = await exists(destination);
156
387
  if (destinationExists && !options.force) {
@@ -161,11 +392,11 @@ async function installTemplates(targetArg, options) {
161
392
  if (!options.dryRun) {
162
393
  await mkdir(path.dirname(destination), { recursive: true });
163
394
  if (preset && file === "AGENTS.md") {
164
- const content = await readFile(source, "utf8");
165
- await writeFile(destination, wrapManagedBlock(file, addStackReference(file, content, preset)));
395
+ const content = await buildInitTemplateContent(file, preset, options.personalization);
396
+ await writeFile(destination, wrapManagedBlock(file, content));
166
397
  }
167
398
  else {
168
- const content = await readFile(source, "utf8");
399
+ const content = await buildInitTemplateContent(file, preset, options.personalization);
169
400
  await writeFile(destination, wrapManagedBlock(file, content));
170
401
  }
171
402
  }
@@ -301,42 +532,95 @@ function printUpdateResult(result, dryRun = false) {
301
532
  }
302
533
  async function resolveInteractiveTarget(target, options) {
303
534
  const providedPreset = resolvePreset(options.preset);
304
- if (!options.interactive) {
535
+ const shouldPrompt = !options.yes && (options.interactive || process.stdin.isTTY);
536
+ if (!shouldPrompt) {
305
537
  return target;
306
538
  }
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);
539
+ intro("Welcome to AgentKit");
540
+ let resolvedTarget = target;
541
+ if (!target || target === ".") {
542
+ const targetResponse = await text({
543
+ message: "Where should AgentKit install files?",
544
+ placeholder: ".",
545
+ defaultValue: ".",
546
+ });
547
+ if (isCancel(targetResponse)) {
548
+ process.exit(130);
549
+ }
550
+ resolvedTarget = targetResponse || ".";
314
551
  }
315
- const forceResponse = await confirm({
316
- message: "Overwrite existing files?",
317
- initialValue: Boolean(options.force),
552
+ if (!providedPreset) {
553
+ const projectTypeResponse = await select({
554
+ message: "What type of project is this?",
555
+ initialValue: "generic",
556
+ options: [
557
+ { label: "Generic TypeScript project", value: "generic" },
558
+ { label: "Next.js app", value: "next" },
559
+ { label: "SvelteKit app", value: "sveltekit" },
560
+ { label: "Express API", value: "express" },
561
+ { label: "Convex app", value: "convex" },
562
+ { label: "Fullstack app", value: "fullstack" },
563
+ ],
564
+ });
565
+ if (isCancel(projectTypeResponse)) {
566
+ process.exit(130);
567
+ }
568
+ options.preset = resolveProjectPreset(projectTypeResponse);
569
+ }
570
+ const aiToolResponse = await multiselect({
571
+ message: "Which AI tools do you use?",
572
+ initialValues: ["codex", "cursor", "claude"],
573
+ options: [
574
+ { label: "Codex", value: "codex" },
575
+ { label: "Cursor", value: "cursor" },
576
+ { label: "Claude Code", value: "claude" },
577
+ { label: "GitHub Copilot", value: "copilot" },
578
+ ],
318
579
  });
319
- if (isCancel(forceResponse)) {
580
+ if (isCancel(aiToolResponse)) {
320
581
  process.exit(130);
321
582
  }
322
- const presetResponse = await select({
323
- message: "Which preset should AgentKit use?",
324
- initialValue: providedPreset || "generic",
583
+ const templateSetResponse = await select({
584
+ message: "Which template set do you want?",
585
+ initialValue: "standard",
325
586
  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" },
587
+ { label: "Minimal", value: "minimal" },
588
+ { label: "Standard", value: "standard" },
589
+ { label: "Full", value: "full" },
332
590
  ],
333
591
  });
334
- if (isCancel(presetResponse)) {
592
+ if (isCancel(templateSetResponse)) {
335
593
  process.exit(130);
336
594
  }
337
- options.preset = presetResponse === "generic" ? undefined : presetResponse;
338
- options.force = forceResponse;
339
- return targetResponse || ".";
595
+ const templateFiles = await getTemplateFiles();
596
+ options.files = getSelectedTemplateFiles(templateSetResponse, aiToolResponse, templateFiles);
597
+ if (!options.force) {
598
+ const preset = resolvePreset(options.preset);
599
+ const installFiles = preset ? [...options.files, "STACK.md"] : options.files;
600
+ const targetDir = path.resolve(process.cwd(), resolvedTarget || ".");
601
+ const existingFiles = [];
602
+ for (const file of installFiles) {
603
+ if (await exists(path.join(targetDir, file))) {
604
+ existingFiles.push(file);
605
+ }
606
+ }
607
+ if (existingFiles.length > 0) {
608
+ const conflictResponse = await select({
609
+ message: `Existing files found: ${existingFiles.join(", ")}. How should AgentKit handle conflicts?`,
610
+ initialValue: "skip",
611
+ options: [
612
+ { label: "Skip existing files", value: "skip" },
613
+ { label: "Overwrite existing files", value: "overwrite" },
614
+ ],
615
+ });
616
+ if (isCancel(conflictResponse)) {
617
+ process.exit(130);
618
+ }
619
+ options.force = conflictResponse === "overwrite";
620
+ }
621
+ }
622
+ options.personalization = await promptForPersonalization();
623
+ return resolvedTarget || ".";
340
624
  }
341
625
  async function main() {
342
626
  if (process.argv.slice(2).includes("--list")) {
@@ -365,7 +649,7 @@ Examples:
365
649
  agentkit init
366
650
  agentkit update
367
651
  agentkit init --preset next
368
- agentkit init ./my-project --dry-run
652
+ agentkit init ./my-project --yes --dry-run
369
653
  agentkit --list-presets
370
654
  agentkit --list`);
371
655
  program
@@ -394,8 +678,10 @@ Examples:
394
678
  });
395
679
  await program.parseAsync(process.argv);
396
680
  }
397
- main().catch((error) => {
398
- const message = error instanceof Error ? error.message : String(error);
399
- console.error(message);
400
- process.exitCode = 1;
401
- });
681
+ if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
682
+ main().catch((error) => {
683
+ const message = error instanceof Error ? error.message : String(error);
684
+ console.error(message);
685
+ process.exitCode = 1;
686
+ });
687
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thomas-agentkit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Install AI-agent-ready development templates into a project.",
5
5
  "license": "MIT",
6
6
  "repository": {