create-nudge-ui 0.1.3 → 0.2.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.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # `create-nudge-ui`
2
2
 
3
3
  `create-nudge-ui` detects an application's host framework, installs the
4
- corresponding Nudge UI adapter, and updates the host configuration.
4
+ corresponding Nudge UI adapter, and updates the host configuration. The guided
5
+ setup can also connect a coding agent to the project-local Nudge MCP server.
5
6
 
6
7
  Run the initializer from the application root:
7
8
 
@@ -11,7 +12,16 @@ npm create nudge-ui@latest
11
12
 
12
13
  The initializer supports Next.js, Astro, Vite with React, and static HTML. Host
13
14
  frameworks take precedence over their underlying tools, so an Astro project
14
- with React islands receives `@nudge-ui/astro`, not `@nudge-ui/vite-react`.
15
+ with React islands receives `nudge-ui/astro`, not `nudge-ui/vite`.
16
+
17
+ In an interactive terminal, the initializer asks whether to connect a coding
18
+ agent. It detects installed agents, installs the matching `@nudge-ui/mcp`
19
+ version in the project, and adds a project-scoped MCP entry. The entry launches
20
+ the package from this project's `node_modules` directory and passes the
21
+ canonical workspace path, so different Git worktrees do not share a server
22
+ configuration.
23
+
24
+ Reload an agent that was already running, then ask it to “listen to Nudge.”
15
25
 
16
26
  Use an explicit framework when automatic detection is ambiguous:
17
27
 
@@ -22,3 +32,28 @@ npm create nudge-ui@latest -- --framework astro
22
32
  Use `--dry-run` to print the package installation and configuration change
23
33
  without modifying the project. Run `npm create nudge-ui@latest -- --help` for
24
34
  the complete command reference.
35
+
36
+ For scripts, select the MCP behavior explicitly:
37
+
38
+ ```sh
39
+ # Configure detected agents without prompts.
40
+ npm create nudge-ui@latest -- --mcp --yes
41
+
42
+ # Configure one or more supported add-mcp agent IDs.
43
+ npm create nudge-ui@latest -- --mcp --agent codex --agent cursor --yes
44
+
45
+ # Skip agent setup.
46
+ npm create nudge-ui@latest -- --no-mcp
47
+ ```
48
+
49
+ Use agent-only mode to add or repair the integration without changing the host
50
+ framework configuration:
51
+
52
+ ```sh
53
+ npm create nudge-ui@latest -- --agent-only --agent codex --yes
54
+ ```
55
+
56
+ Agent configuration is best-effort. If an agent is not supported or its native
57
+ configuration cannot be updated, the initializer prints the complete stdio
58
+ configuration to add to that agent's project settings. Other MCP servers and
59
+ agent settings are preserved on every run.
@@ -0,0 +1,35 @@
1
+ import { type AgentType, type InstallResult, type McpServerConfig } from "add-mcp";
2
+ import { type InstallCommand } from "./installer.js";
3
+ import type { PackageManager } from "./types.js";
4
+ export declare const nudgeServerName = "nudge_ui";
5
+ export interface AgentSetupPlan {
6
+ readonly projectRoot: string;
7
+ readonly packageSpecifier: string;
8
+ readonly installCommand: InstallCommand;
9
+ readonly serverConfig: McpServerConfig;
10
+ readonly serverEntry?: string;
11
+ }
12
+ export interface AgentInstallOutcome {
13
+ readonly agent: string;
14
+ readonly displayName: string;
15
+ readonly result?: InstallResult;
16
+ readonly unsupported: boolean;
17
+ }
18
+ /** Builds the reproducible, project-local MCP installation and launch plan. */
19
+ export declare function planAgentSetup(projectRoot: string, packageManager: PackageManager, nudgeUiVersion?: string): AgentSetupPlan;
20
+ /** Checks that a node_modules based install produced the configured executable. */
21
+ export declare function isAgentServerAvailable(plan: AgentSetupPlan): boolean;
22
+ /** Returns agents that can store an MCP configuration in this project. */
23
+ export declare function projectAgentTypes(): AgentType[];
24
+ /** Detects installed or already configured agents while preserving project scope. */
25
+ export declare function detectProjectAgentCandidates(projectRoot: string): Promise<AgentType[]>;
26
+ /** Adds or repairs Nudge's project-scoped MCP entry without replacing other servers. */
27
+ export declare function configureProjectAgents(plan: AgentSetupPlan, selectedAgents: readonly string[]): AgentInstallOutcome[];
28
+ /** Formats a complete stdio entry for agents that add-mcp cannot configure. */
29
+ export declare function manualAgentConfiguration(plan: AgentSetupPlan): string;
30
+ export declare function projectAgentChoices(detected: readonly AgentType[]): Array<{
31
+ readonly id: AgentType;
32
+ readonly label: string;
33
+ readonly detected: boolean;
34
+ }>;
35
+ export declare function initializerVersion(): string;
@@ -0,0 +1,104 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { agents, detectGlobalAgents, detectProjectAgents, getAgentTypes, upsertServer, } from "add-mcp";
4
+ import { installCommand } from "./installer.js";
5
+ export const nudgeServerName = "nudge_ui";
6
+ /** Builds the reproducible, project-local MCP installation and launch plan. */
7
+ export function planAgentSetup(projectRoot, packageManager, nudgeUiVersion) {
8
+ const canonicalRoot = realpathSync.native(projectRoot);
9
+ const packageSpecifier = `@nudge-ui/mcp@${nudgeUiVersion ?? installedNudgeUiVersion(canonicalRoot) ?? initializerVersion()}`;
10
+ const serverEntry = join(canonicalRoot, "node_modules", "@nudge-ui", "mcp", "dist", "cli.mjs");
11
+ const usesPlugAndPlay = packageManager === "yarn";
12
+ const serverConfig = usesPlugAndPlay
13
+ ? {
14
+ command: "yarn",
15
+ args: ["--cwd", canonicalRoot, "exec", "nudge-mcp", "--workspace-root", canonicalRoot],
16
+ }
17
+ : {
18
+ command: "node",
19
+ args: [serverEntry, "--workspace-root", canonicalRoot],
20
+ };
21
+ return {
22
+ projectRoot: canonicalRoot,
23
+ packageSpecifier,
24
+ installCommand: installCommand(packageManager, packageSpecifier),
25
+ serverConfig,
26
+ serverEntry: usesPlugAndPlay ? undefined : serverEntry,
27
+ };
28
+ }
29
+ /** Checks that a node_modules based install produced the configured executable. */
30
+ export function isAgentServerAvailable(plan) {
31
+ return plan.serverEntry === undefined || existsSync(plan.serverEntry);
32
+ }
33
+ /** Returns agents that can store an MCP configuration in this project. */
34
+ export function projectAgentTypes() {
35
+ return getAgentTypes().filter((agent) => agents[agent].localConfigPath !== undefined);
36
+ }
37
+ /** Detects installed or already configured agents while preserving project scope. */
38
+ export async function detectProjectAgentCandidates(projectRoot) {
39
+ const supported = new Set(projectAgentTypes());
40
+ const detected = [
41
+ ...detectProjectAgents(projectRoot),
42
+ ...await detectGlobalAgents(),
43
+ ].filter((agent) => supported.has(agent));
44
+ return [...new Set(detected)];
45
+ }
46
+ /** Adds or repairs Nudge's project-scoped MCP entry without replacing other servers. */
47
+ export function configureProjectAgents(plan, selectedAgents) {
48
+ const supported = new Set(projectAgentTypes());
49
+ return [...new Set(selectedAgents)].map((agent) => {
50
+ if (!supported.has(agent)) {
51
+ return { agent, displayName: agent, unsupported: true };
52
+ }
53
+ const agentType = agent;
54
+ const result = upsertServer(agentType, nudgeServerName, plan.serverConfig, {
55
+ cwd: plan.projectRoot,
56
+ local: true,
57
+ });
58
+ return {
59
+ agent,
60
+ displayName: agents[agentType].displayName,
61
+ result,
62
+ unsupported: false,
63
+ };
64
+ });
65
+ }
66
+ /** Formats a complete stdio entry for agents that add-mcp cannot configure. */
67
+ export function manualAgentConfiguration(plan) {
68
+ return JSON.stringify({
69
+ mcpServers: {
70
+ [nudgeServerName]: plan.serverConfig,
71
+ },
72
+ }, null, 2);
73
+ }
74
+ export function projectAgentChoices(detected) {
75
+ const detectedSet = new Set(detected);
76
+ return projectAgentTypes()
77
+ .sort((left, right) => {
78
+ const detectionOrder = Number(detectedSet.has(right)) - Number(detectedSet.has(left));
79
+ return detectionOrder || agents[left].displayName.localeCompare(agents[right].displayName);
80
+ })
81
+ .map((id) => ({ id, label: agents[id].displayName, detected: detectedSet.has(id) }));
82
+ }
83
+ export function initializerVersion() {
84
+ const packageJsonUrl = new URL("../package.json", import.meta.url);
85
+ const value = JSON.parse(readFileSync(packageJsonUrl, "utf8"));
86
+ if (!isPackageMetadata(value))
87
+ throw new TypeError("create-nudge-ui package metadata has no version.");
88
+ return value.version;
89
+ }
90
+ function installedNudgeUiVersion(projectRoot) {
91
+ const manifestPath = join(projectRoot, "node_modules", "nudge-ui", "package.json");
92
+ try {
93
+ const value = JSON.parse(readFileSync(manifestPath, "utf8"));
94
+ return isPackageMetadata(value) ? value.version : undefined;
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ function isPackageMetadata(value) {
101
+ if (typeof value !== "object" || value === null || Array.isArray(value))
102
+ return false;
103
+ return typeof value.version === "string";
104
+ }
@@ -2,9 +2,13 @@ import { type Framework, type PackageManager } from "./types.js";
2
2
  export interface CliOptions {
3
3
  readonly framework?: Framework;
4
4
  readonly packageManager?: PackageManager;
5
+ readonly agentOnly: boolean;
6
+ readonly agents: readonly string[];
7
+ readonly mcp?: boolean;
8
+ readonly yes: boolean;
5
9
  readonly dryRun: boolean;
6
10
  readonly help: boolean;
7
11
  }
8
12
  /** Parses the initializer command line. */
9
13
  export declare function parseArguments(args: readonly string[]): CliOptions;
10
- export declare const helpText = "Set up Nudge UI in the current project.\n\nUsage: npm create nudge-ui@latest -- [options]\n\nOptions:\n --framework <nextjs|astro|vite-react|standalone>\n --package-manager <pnpm|npm|yarn|bun>\n --dry-run\n -h, --help";
14
+ export declare const helpText = "Set up Nudge UI in the current project.\n\nUsage: npm create nudge-ui@latest -- [options]\n\nOptions:\n --framework <nextjs|astro|vite-react|standalone>\n --package-manager <pnpm|npm|yarn|bun>\n --mcp Configure the local Nudge MCP server\n --no-mcp Skip coding-agent setup\n --agent <id> Configure a supported agent (repeatable)\n --agent-only Install or repair only the agent integration\n -y, --yes Accept defaults without prompting\n --dry-run\n -h, --help";
package/dist/arguments.js CHANGED
@@ -3,14 +3,35 @@ import { frameworks, packageManagers } from "./types.js";
3
3
  export function parseArguments(args) {
4
4
  let framework;
5
5
  let packageManager;
6
+ let agentOnly = false;
7
+ const agents = [];
8
+ let mcp;
9
+ let yes = false;
6
10
  let dryRun = false;
7
11
  let help = false;
8
12
  for (let index = 0; index < args.length; index += 1) {
9
13
  const argument = args[index];
10
14
  if (argument === "--")
11
15
  continue;
16
+ if (index === 0 && argument === "agent" && args[index + 1] === "setup") {
17
+ agentOnly = true;
18
+ index += 1;
19
+ continue;
20
+ }
12
21
  if (argument === "--dry-run")
13
22
  dryRun = true;
23
+ else if (argument === "--yes" || argument === "-y")
24
+ yes = true;
25
+ else if (argument === "--agent-only")
26
+ agentOnly = true;
27
+ else if (argument === "--mcp")
28
+ mcp = setMcpPreference(mcp, true);
29
+ else if (argument === "--no-mcp")
30
+ mcp = setMcpPreference(mcp, false);
31
+ else if (argument === "--agent")
32
+ agents.push(requiredValue(args, ++index, argument));
33
+ else if (argument.startsWith("--agent="))
34
+ agents.push(requiredValue([argument.slice("--agent=".length)], 0, "--agent"));
14
35
  else if (argument === "--help" || argument === "-h")
15
36
  help = true;
16
37
  else if (argument === "--framework")
@@ -26,7 +47,16 @@ export function parseArguments(args) {
26
47
  else
27
48
  throw new Error(`Unknown option: ${argument}`);
28
49
  }
29
- return { framework, packageManager, dryRun, help };
50
+ if (agents.length > 0 && mcp === false)
51
+ throw new Error("--agent cannot be combined with --no-mcp.");
52
+ if (agentOnly && mcp === false)
53
+ throw new Error("--agent-only cannot be combined with --no-mcp.");
54
+ return { framework, packageManager, agentOnly, agents, mcp, yes, dryRun, help };
55
+ }
56
+ function setMcpPreference(current, next) {
57
+ if (current !== undefined && current !== next)
58
+ throw new Error("--mcp and --no-mcp cannot be combined.");
59
+ return next;
30
60
  }
31
61
  function requiredValue(args, index, option) {
32
62
  const value = args[index];
@@ -41,6 +71,11 @@ Usage: npm create nudge-ui@latest -- [options]
41
71
  Options:
42
72
  --framework <nextjs|astro|vite-react|standalone>
43
73
  --package-manager <pnpm|npm|yarn|bun>
74
+ --mcp Configure the local Nudge MCP server
75
+ --no-mcp Skip coding-agent setup
76
+ --agent <id> Configure a supported agent (repeatable)
77
+ --agent-only Install or repair only the agent integration
78
+ -y, --yes Accept defaults without prompting
44
79
  --dry-run
45
80
  -h, --help`;
46
81
  function parseFramework(value) {
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,12 @@
1
+ import { type PackageManager } from "./types.js";
2
+ export interface RunAgentSetupOptions {
3
+ readonly agents?: readonly string[];
4
+ readonly dryRun?: boolean;
5
+ readonly nudgeUiVersion?: string;
6
+ readonly packageManager?: PackageManager;
7
+ readonly yes?: boolean;
8
+ }
1
9
  /** Runs the Nudge UI project initializer. */
2
10
  export declare function runCli(args?: string[], projectRoot?: string): Promise<void>;
11
+ /** Installs and configures the project-local MCP companion. Safe to rerun for repairs. */
12
+ export declare function runAgentSetup(projectRoot?: string, options?: RunAgentSetupOptions): Promise<void>;
package/dist/cli.js CHANGED
@@ -2,11 +2,12 @@ import { rmSync, writeFileSync } from "node:fs";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { stdin, stdout } from "node:process";
4
4
  import { basename, relative } from "node:path";
5
+ import { configureProjectAgents, detectProjectAgentCandidates, initializerVersion, isAgentServerAvailable, manualAgentConfiguration, planAgentSetup, projectAgentChoices, } from "./agent-setup.js";
5
6
  import { parseArguments, helpText } from "./arguments.js";
6
7
  import { planConfiguration } from "./configuration.js";
7
- import { adapterPackage, detectFrameworks, detectPackageManager, detectStaticRoot, frameworkDisplayName, readProjectManifest, } from "./detection.js";
8
+ import { detectFrameworks, detectPackageManager, detectStaticRoot, frameworkDisplayName, readProjectManifest, } from "./detection.js";
8
9
  import { formatCommand, installCommand, installPackage } from "./installer.js";
9
- import { frameworks } from "./types.js";
10
+ import { frameworks, } from "./types.js";
10
11
  /** Runs the Nudge UI project initializer. */
11
12
  export async function runCli(args = process.argv.slice(2), projectRoot = process.cwd()) {
12
13
  const options = parseArguments(args);
@@ -15,11 +16,21 @@ export async function runCli(args = process.argv.slice(2), projectRoot = process
15
16
  return;
16
17
  }
17
18
  const manifest = readProjectManifest(projectRoot);
19
+ const packageManager = options.packageManager ?? detectPackageManager(projectRoot, manifest);
20
+ if (options.agentOnly) {
21
+ await runAgentSetup(projectRoot, {
22
+ agents: options.agents,
23
+ dryRun: options.dryRun,
24
+ packageManager,
25
+ yes: options.yes,
26
+ });
27
+ return;
28
+ }
18
29
  const detected = detectFrameworks(projectRoot, manifest);
19
30
  const framework = options.framework ?? await selectFramework(detected);
20
- const packageManager = options.packageManager ?? detectPackageManager(projectRoot, manifest);
21
- const packageName = adapterPackage(framework);
22
- const command = installCommand(packageManager, packageName);
31
+ // One package serves every host; the framework only decides which
32
+ // subpath the generated configuration imports.
33
+ const command = installCommand(packageManager, `nudge-ui@${initializerVersion()}`);
23
34
  const change = planConfiguration(projectRoot, framework);
24
35
  const staticRoot = framework === "standalone" ? detectStaticRoot(projectRoot) : undefined;
25
36
  const serveDirectory = staticRoot ? relative(projectRoot, staticRoot) || "." : ".";
@@ -30,6 +41,15 @@ export async function runCli(args = process.argv.slice(2), projectRoot = process
30
41
  stdout.write(`Would ${change.created ? "create" : "update"}: ${change.path}\n`);
31
42
  if (framework === "standalone")
32
43
  stdout.write(`After installation, run: nudge-ui serve ${serveDirectory}\n`);
44
+ if (await shouldConfigureAgent(options.mcp, options.agents.length > 0, options.yes)) {
45
+ await runAgentSetup(projectRoot, {
46
+ agents: options.agents,
47
+ dryRun: true,
48
+ nudgeUiVersion: initializerVersion(),
49
+ packageManager,
50
+ yes: options.yes,
51
+ });
52
+ }
33
53
  return;
34
54
  }
35
55
  if (change) {
@@ -49,7 +69,128 @@ export async function runCli(args = process.argv.slice(2), projectRoot = process
49
69
  if (framework === "standalone") {
50
70
  stdout.write(`Run \`nudge-ui serve ${serveDirectory}\` to start the instrumented development server.\n`);
51
71
  }
52
- stdout.write(`Nudge UI is configured with ${packageName}.\n`);
72
+ stdout.write(`Nudge UI is configured for ${frameworkDisplayName(framework)}.\n`);
73
+ if (await shouldConfigureAgent(options.mcp, options.agents.length > 0, options.yes)) {
74
+ await runAgentSetup(projectRoot, {
75
+ agents: options.agents,
76
+ nudgeUiVersion: initializerVersion(),
77
+ packageManager,
78
+ yes: options.yes,
79
+ });
80
+ }
81
+ }
82
+ /** Installs and configures the project-local MCP companion. Safe to rerun for repairs. */
83
+ export async function runAgentSetup(projectRoot = process.cwd(), options = {}) {
84
+ const manifest = readProjectManifest(projectRoot);
85
+ const packageManager = options.packageManager ?? detectPackageManager(projectRoot, manifest);
86
+ const plan = planAgentSetup(projectRoot, packageManager, options.nudgeUiVersion);
87
+ const detected = await detectProjectAgentCandidates(plan.projectRoot);
88
+ const selected = options.agents && options.agents.length > 0
89
+ ? options.agents
90
+ : options.yes || !stdin.isTTY || !stdout.isTTY
91
+ ? detected
92
+ : await selectAgents(detected);
93
+ if (options.dryRun) {
94
+ stdout.write(`Would run: ${formatCommand(plan.installCommand)}\n`);
95
+ if (selected.length > 0) {
96
+ stdout.write(`Would configure project MCP for: ${selected.join(", ")}\n`);
97
+ }
98
+ else {
99
+ stdout.write("No coding agent was detected. Use --agent <id> or add the configuration below manually.\n");
100
+ writeManualInstructions(plan);
101
+ }
102
+ return;
103
+ }
104
+ installPackage(plan.installCommand, plan.projectRoot);
105
+ if (!isAgentServerAvailable(plan)) {
106
+ stdout.write(`Installed ${plan.packageSpecifier}, but its local MCP executable was not found at ${plan.serverEntry}.\n`);
107
+ stdout.write("The coding-agent configuration was not changed. Check the package-manager install output, then rerun with --agent-only.\n");
108
+ return;
109
+ }
110
+ if (selected.length === 0) {
111
+ stdout.write(`Installed ${plan.packageSpecifier}, but no coding agent was detected.\n`);
112
+ writeManualInstructions(plan);
113
+ return;
114
+ }
115
+ let outcomes;
116
+ try {
117
+ outcomes = configureProjectAgents(plan, selected);
118
+ }
119
+ catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ stdout.write(`Could not update the coding-agent configuration: ${message}\n`);
122
+ writeManualInstructions(plan);
123
+ return;
124
+ }
125
+ let configured = 0;
126
+ for (const outcome of outcomes) {
127
+ if (outcome.unsupported) {
128
+ stdout.write(`${outcome.displayName} is not supported for automatic project setup.\n`);
129
+ continue;
130
+ }
131
+ if (outcome.result?.success) {
132
+ configured += 1;
133
+ stdout.write(`Configured ${outcome.displayName}: ${outcome.result.path}\n`);
134
+ continue;
135
+ }
136
+ stdout.write(`Could not configure ${outcome.displayName}: ${outcome.result?.error ?? "unknown error"}\n`);
137
+ }
138
+ if (configured !== outcomes.length)
139
+ writeManualInstructions(plan);
140
+ if (configured > 0) {
141
+ stdout.write("Coding-agent setup is ready. Reload the agent if it is already open, then ask it to listen to Nudge.\n");
142
+ }
143
+ }
144
+ async function shouldConfigureAgent(preference, hasExplicitAgents, yes) {
145
+ if (preference !== undefined)
146
+ return preference;
147
+ if (hasExplicitAgents || yes)
148
+ return true;
149
+ if (!stdin.isTTY || !stdout.isTTY)
150
+ return false;
151
+ return promptYesNo("Connect a coding agent?", true);
152
+ }
153
+ async function promptYesNo(message, defaultValue) {
154
+ const reader = createInterface({ input: stdin, output: stdout });
155
+ try {
156
+ const answer = (await reader.question(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase();
157
+ if (!answer)
158
+ return defaultValue;
159
+ return answer === "y" || answer === "yes";
160
+ }
161
+ finally {
162
+ reader.close();
163
+ }
164
+ }
165
+ async function selectAgents(detected) {
166
+ const choices = projectAgentChoices(detected);
167
+ stdout.write("Select coding agents (comma-separated):\n");
168
+ choices.forEach((choice, index) => {
169
+ stdout.write(` ${index + 1}. ${choice.label}${choice.detected ? " (detected)" : ""}\n`);
170
+ });
171
+ const defaults = choices
172
+ .map((choice, index) => choice.detected ? String(index + 1) : undefined)
173
+ .filter((value) => value !== undefined)
174
+ .join(",");
175
+ const reader = createInterface({ input: stdin, output: stdout });
176
+ try {
177
+ while (true) {
178
+ const suffix = defaults ? ` [${defaults}]` : "";
179
+ const answer = (await reader.question(`Agents${suffix}: `)).trim() || defaults;
180
+ const indexes = answer.split(",").map((value) => Number(value.trim()) - 1);
181
+ if (indexes.length > 0 && indexes.every((index) => choices[index])) {
182
+ return [...new Set(indexes.map((index) => choices[index].id))];
183
+ }
184
+ stdout.write(`Enter one or more numbers from 1 to ${choices.length}.\n`);
185
+ }
186
+ }
187
+ finally {
188
+ reader.close();
189
+ }
190
+ }
191
+ function writeManualInstructions(plan) {
192
+ stdout.write("Add this project-scoped stdio server to your agent's MCP settings:\n");
193
+ stdout.write(`${manualAgentConfiguration(plan)}\n`);
53
194
  }
54
195
  async function selectFramework(detected) {
55
196
  if (detected.length === 1)
@@ -3,7 +3,20 @@ import { join } from "node:path";
3
3
  import ts from "typescript";
4
4
  const nextConfigNames = ["next.config.ts", "next.config.mts", "next.config.mjs", "next.config.js", "next.config.cjs"];
5
5
  const astroConfigNames = ["astro.config.ts", "astro.config.mts", "astro.config.mjs", "astro.config.js"];
6
- const viteConfigNames = ["vite.config.ts", "vite.config.mts", "vite.config.mjs", "vite.config.js"];
6
+ /**
7
+ * Vite's own `DEFAULT_CONFIG_FILES` order. The list is order-sensitive: the
8
+ * first existing file wins for Vite, so it must win here too, or the installer
9
+ * edits a configuration Vite never loads (and creating a new `vite.config.mjs`
10
+ * beside a `vite.config.cjs` would silently shadow it).
11
+ */
12
+ const viteConfigNames = [
13
+ "vite.config.js",
14
+ "vite.config.mjs",
15
+ "vite.config.ts",
16
+ "vite.config.cjs",
17
+ "vite.config.mts",
18
+ "vite.config.cts",
19
+ ];
7
20
  /** Plans an idempotent host-configuration edit without writing to the project. */
8
21
  export function planConfiguration(projectRoot, framework) {
9
22
  if (framework === "standalone")
@@ -22,45 +35,59 @@ export function planConfiguration(projectRoot, framework) {
22
35
  export function configureSource(source, framework, fileName) {
23
36
  const sourceFile = parseSource(source, fileName);
24
37
  if (framework === "astro") {
25
- return configureArrayHost(source, sourceFile, "@nudge-ui/astro", "nudgeUiAstro", "integrations", "nudgeUiAstro()");
38
+ return configureAstroSource(source, sourceFile);
26
39
  }
27
40
  if (framework === "vite-react") {
28
- return configureArrayHost(source, sourceFile, "@nudge-ui/vite-react", "nudgeUi", "plugins", "...nudgeUi()");
41
+ return configureViteSource(source, sourceFile);
29
42
  }
30
43
  return configureNextSource(source, sourceFile);
31
44
  }
32
- function configureArrayHost(source, sourceFile, packageName, importName, propertyName, expression) {
33
- const bindings = importedBindings(sourceFile, packageName, importName);
34
- const callableBindings = new Set([...bindings, importName]);
35
- const configObject = findDefineConfigObject(sourceFile, propertyName);
36
- const matchingProperties = configObject.properties.filter((property) => propertyText(property.name) === propertyName);
37
- if (matchingProperties.length > 1) {
38
- throw new Error(`Could not update ${propertyName}: the configuration contains duplicate ${propertyName} properties.`);
45
+ function configureViteSource(source, sourceFile) {
46
+ const packageName = "nudge-ui/vite";
47
+ const importName = "withNudgeUi";
48
+ const target = findConfigExport(sourceFile, "Vite");
49
+ const bindings = target.commonJs
50
+ ? requiredBindings(sourceFile, packageName, importName)
51
+ : importedBindings(sourceFile, packageName, importName);
52
+ const wrapperBinding = bindings[0] ?? unusedBindingName(sourceFile, importName);
53
+ const callableBindings = new Set([...bindings, wrapperBinding]);
54
+ const edits = [];
55
+ if (!containsCall(target.expression, callableBindings)) {
56
+ const start = target.expression.getStart(sourceFile);
57
+ const end = target.expression.getEnd();
58
+ edits.push({ start, end, text: `${wrapperBinding}(${source.slice(start, end)})` });
39
59
  }
40
- const property = matchingProperties[0];
41
- let configurationEdit;
42
- if (property) {
43
- if (!ts.isPropertyAssignment(property) || !ts.isArrayLiteralExpression(property.initializer)) {
44
- throw new Error(`Could not update ${propertyName}: expected ${propertyName} to be an array.`);
45
- }
46
- if (!containsCall(property.initializer, callableBindings)) {
47
- configurationEdit = insertArrayElement(source, sourceFile, property, expression);
48
- }
60
+ if (bindings.length === 0) {
61
+ const statement = target.commonJs
62
+ ? `const { ${importName}${wrapperBinding === importName ? "" : `: ${wrapperBinding}`} } = require("${packageName}");`
63
+ : `import { ${importName}${wrapperBinding === importName ? "" : ` as ${wrapperBinding}`} } from "${packageName}";`;
64
+ edits.push(importEdit(sourceFile, statement, target.commonJs));
49
65
  }
50
- else {
51
- configurationEdit = insertObjectProperty(source, sourceFile, configObject, propertyName, expression);
66
+ return applyEdits(source, edits);
67
+ }
68
+ function configureAstroSource(source, sourceFile) {
69
+ const target = findDefaultExport(sourceFile, "Astro");
70
+ const bindings = importedBindings(sourceFile, "nudge-ui/astro", "withNudgeUi");
71
+ const wrapperBinding = bindings[0] ?? unusedBindingName(sourceFile, "withNudgeUi");
72
+ const edits = [];
73
+ if (!containsCall(target, new Set(bindings))) {
74
+ const start = target.getStart(sourceFile);
75
+ const end = target.getEnd();
76
+ edits.push({ start, end, text: `${wrapperBinding}(${source.slice(start, end)})` });
52
77
  }
53
- const edits = configurationEdit ? [configurationEdit] : [];
54
78
  if (bindings.length === 0) {
55
- edits.push(importEdit(sourceFile, `import { ${importName} } from "${packageName}";`, false));
79
+ const importedName = wrapperBinding === "withNudgeUi"
80
+ ? "withNudgeUi"
81
+ : `withNudgeUi as ${wrapperBinding}`;
82
+ edits.push(importEdit(sourceFile, `import { ${importedName} } from "nudge-ui/astro";`, false));
56
83
  }
57
84
  return applyEdits(source, edits);
58
85
  }
59
86
  function configureNextSource(source, sourceFile) {
60
- const target = findNextExport(sourceFile);
87
+ const target = findConfigExport(sourceFile, "Next.js");
61
88
  const bindings = target.commonJs
62
- ? requiredBindings(sourceFile, "@nudge-ui/nextjs", "withNudgeUi")
63
- : importedBindings(sourceFile, "@nudge-ui/nextjs", "withNudgeUi");
89
+ ? requiredBindings(sourceFile, "nudge-ui/next", "withNudgeUi")
90
+ : importedBindings(sourceFile, "nudge-ui/next", "withNudgeUi");
64
91
  const callableBindings = new Set([...bindings, "withNudgeUi"]);
65
92
  const edits = [];
66
93
  if (!containsCall(target.expression, callableBindings)) {
@@ -70,34 +97,19 @@ function configureNextSource(source, sourceFile) {
70
97
  }
71
98
  if (bindings.length === 0) {
72
99
  const statement = target.commonJs
73
- ? 'const { withNudgeUi } = require("@nudge-ui/nextjs");'
74
- : 'import { withNudgeUi } from "@nudge-ui/nextjs";';
100
+ ? 'const { withNudgeUi } = require("nudge-ui/next");'
101
+ : 'import { withNudgeUi } from "nudge-ui/next";';
75
102
  edits.push(importEdit(sourceFile, statement, target.commonJs));
76
103
  }
77
104
  return applyEdits(source, edits);
78
105
  }
79
106
  function parseSource(source, fileName) {
80
- const scriptKind = fileName.endsWith(".ts") || fileName.endsWith(".mts")
107
+ const scriptKind = /\.(?:ts|mts|cts)$/.test(fileName)
81
108
  ? ts.ScriptKind.TS
82
109
  : ts.ScriptKind.JS;
83
110
  return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKind);
84
111
  }
85
- function findDefineConfigObject(sourceFile, propertyName) {
86
- const candidates = [];
87
- visit(sourceFile, (node) => {
88
- if (ts.isCallExpression(node)
89
- && ts.isIdentifier(node.expression)
90
- && node.expression.text === "defineConfig"
91
- && node.arguments[0]
92
- && ts.isObjectLiteralExpression(node.arguments[0]))
93
- candidates.push(node.arguments[0]);
94
- });
95
- if (candidates.length !== 1) {
96
- throw new Error(`Could not update ${propertyName}: expected exactly one defineConfig({ ... }) call.`);
97
- }
98
- return candidates[0];
99
- }
100
- function findNextExport(sourceFile) {
112
+ function findConfigExport(sourceFile, frameworkName) {
101
113
  const exports = [];
102
114
  for (const statement of sourceFile.statements) {
103
115
  if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
@@ -112,7 +124,16 @@ function findNextExport(sourceFile) {
112
124
  exports.push({ expression: assignment.right, commonJs: true });
113
125
  }
114
126
  if (exports.length !== 1) {
115
- throw new Error("Could not update the Next.js configuration: expected exactly one default or module.exports assignment.");
127
+ throw new Error(`Could not update the ${frameworkName} configuration: expected exactly one default or module.exports assignment.`);
128
+ }
129
+ return exports[0];
130
+ }
131
+ function findDefaultExport(sourceFile, frameworkName) {
132
+ const exports = sourceFile.statements
133
+ .filter((statement) => ts.isExportAssignment(statement) && !statement.isExportEquals)
134
+ .map((statement) => statement.expression);
135
+ if (exports.length !== 1) {
136
+ throw new Error(`Could not update the ${frameworkName} configuration: expected exactly one default export.`);
116
137
  }
117
138
  return exports[0];
118
139
  }
@@ -180,46 +201,23 @@ function containsCall(root, bindingNames) {
180
201
  });
181
202
  return found;
182
203
  }
204
+ function unusedBindingName(sourceFile, preferred) {
205
+ const identifiers = new Set();
206
+ visit(sourceFile, (node) => {
207
+ if (ts.isIdentifier(node))
208
+ identifiers.add(node.text);
209
+ });
210
+ if (!identifiers.has(preferred))
211
+ return preferred;
212
+ let suffix = 2;
213
+ while (identifiers.has(`${preferred}${suffix}`))
214
+ suffix += 1;
215
+ return `${preferred}${suffix}`;
216
+ }
183
217
  function visit(root, visitor) {
184
218
  visitor(root);
185
219
  ts.forEachChild(root, (child) => visit(child, visitor));
186
220
  }
187
- function propertyText(name) {
188
- if (!name)
189
- return undefined;
190
- if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
191
- return name.text;
192
- if (ts.isComputedPropertyName(name) && ts.isStringLiteral(name.expression))
193
- return name.expression.text;
194
- return undefined;
195
- }
196
- function insertArrayElement(source, sourceFile, property, expression) {
197
- const array = property.initializer;
198
- if (!ts.isArrayLiteralExpression(array))
199
- throw new Error("Expected an array property.");
200
- const start = array.getStart(sourceFile) + 1;
201
- const end = array.getEnd() - 1;
202
- const indentation = lineIndentation(source, property.getStart(sourceFile));
203
- const elementIndentation = `${indentation} `;
204
- const interior = source.slice(start, end);
205
- if (interior.trim() === "") {
206
- return { start, end, text: `\n${elementIndentation}${expression},\n${indentation}` };
207
- }
208
- const text = interior.startsWith("\n")
209
- ? `\n${elementIndentation}${expression},`
210
- : `\n${elementIndentation}${expression},\n${elementIndentation}`;
211
- return { start, end: start, text };
212
- }
213
- function insertObjectProperty(source, sourceFile, object, propertyName, expression) {
214
- const start = object.getStart(sourceFile) + 1;
215
- const indentation = lineIndentation(source, object.getStart(sourceFile));
216
- const propertyIndentation = `${indentation} `;
217
- const empty = object.properties.length === 0;
218
- const text = empty
219
- ? `\n${propertyIndentation}${propertyName}: [${expression}],\n${indentation}`
220
- : `\n${propertyIndentation}${propertyName}: [${expression}],`;
221
- return { start, end: start, text };
222
- }
223
221
  function importEdit(sourceFile, statement, commonJs) {
224
222
  const imports = sourceFile.statements.filter(ts.isImportDeclaration);
225
223
  const lastImport = imports.at(-1);
@@ -235,11 +233,6 @@ function importEdit(sourceFile, statement, commonJs) {
235
233
  const start = firstStatement?.getStart(sourceFile) ?? sourceFile.getEnd();
236
234
  return { start, end: start, text: `${statement}\n` };
237
235
  }
238
- function lineIndentation(source, position) {
239
- const lineStart = source.lastIndexOf("\n", position - 1) + 1;
240
- const match = /^[ \t]*/.exec(source.slice(lineStart, position));
241
- return match?.[0] ?? "";
242
- }
243
236
  function applyEdits(source, edits) {
244
237
  return [...edits]
245
238
  .sort((left, right) => right.start - left.start)
@@ -8,4 +8,3 @@ export declare function detectPackageManager(projectRoot: string, manifest?: Pro
8
8
  /** Finds an HTML document root without recursively scanning dependency or build output. */
9
9
  export declare function detectStaticRoot(projectRoot: string): string | undefined;
10
10
  export declare function frameworkDisplayName(framework: Framework): string;
11
- export declare function adapterPackage(framework: Framework): string;
package/dist/detection.js CHANGED
@@ -57,7 +57,7 @@ export function detectFrameworks(projectRoot, manifest = readProjectManifest(pro
57
57
  return ["vite-react"];
58
58
  if (hasVite)
59
59
  return [];
60
- if (manifest.dependencies.has("@nudge-ui/standalone") || detectStaticRoot(projectRoot))
60
+ if (manifest.dependencies.has("nudge-ui") || detectStaticRoot(projectRoot))
61
61
  return ["standalone"];
62
62
  return [];
63
63
  }
@@ -107,15 +107,6 @@ export function frameworkDisplayName(framework) {
107
107
  return "Vite with React";
108
108
  return "static HTML";
109
109
  }
110
- export function adapterPackage(framework) {
111
- if (framework === "nextjs")
112
- return "@nudge-ui/nextjs";
113
- if (framework === "astro")
114
- return "@nudge-ui/astro";
115
- if (framework === "vite-react")
116
- return "@nudge-ui/vite-react";
117
- return "@nudge-ui/standalone";
118
- }
119
110
  function hasAnyFile(projectRoot, names) {
120
111
  return names.some((name) => existsSync(join(projectRoot, name)));
121
112
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { parseArguments, helpText, type CliOptions } from "./arguments.js";
2
+ export { configureProjectAgents, detectProjectAgentCandidates, initializerVersion, isAgentServerAvailable, manualAgentConfiguration, planAgentSetup, projectAgentChoices, projectAgentTypes, type AgentInstallOutcome, type AgentSetupPlan, } from "./agent-setup.js";
3
+ export { runAgentSetup, type RunAgentSetupOptions } from "./cli.js";
2
4
  export { configureSource, planConfiguration } from "./configuration.js";
3
- export { adapterPackage, detectFrameworks, detectPackageManager, detectStaticRoot, frameworkDisplayName, readProjectManifest, } from "./detection.js";
5
+ export { detectFrameworks, detectPackageManager, detectStaticRoot, frameworkDisplayName, readProjectManifest, } from "./detection.js";
4
6
  export { formatCommand, installCommand, type InstallCommand } from "./installer.js";
5
7
  export { frameworks, packageManagers, type Framework, type PackageManager } from "./types.js";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export { parseArguments, helpText } from "./arguments.js";
2
+ export { configureProjectAgents, detectProjectAgentCandidates, initializerVersion, isAgentServerAvailable, manualAgentConfiguration, planAgentSetup, projectAgentChoices, projectAgentTypes, } from "./agent-setup.js";
3
+ export { runAgentSetup } from "./cli.js";
2
4
  export { configureSource, planConfiguration } from "./configuration.js";
3
- export { adapterPackage, detectFrameworks, detectPackageManager, detectStaticRoot, frameworkDisplayName, readProjectManifest, } from "./detection.js";
5
+ export { detectFrameworks, detectPackageManager, detectStaticRoot, frameworkDisplayName, readProjectManifest, } from "./detection.js";
4
6
  export { formatCommand, installCommand } from "./installer.js";
5
7
  export { frameworks, packageManagers } from "./types.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nudge-ui",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Framework-detecting installer for Nudge UI.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,6 +29,7 @@
29
29
  "create-nudge-ui": "./dist/entry.js"
30
30
  },
31
31
  "dependencies": {
32
+ "add-mcp": "^2.4.0",
32
33
  "typescript": "^5.9.3"
33
34
  },
34
35
  "devDependencies": {