create-clicksmith 0.1.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.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/chunk-WEMLICDQ.js +295 -0
- package/dist/chunk-WEMLICDQ.js.map +1 -0
- package/dist/cli.js +76 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ClickSmith contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# create-clicksmith
|
|
2
|
+
|
|
3
|
+
The one-command ClickSmith installer.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm dlx create-clicksmith # or: npm create clicksmith
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It detects your stack and wires everything up **without clobbering your files**:
|
|
10
|
+
|
|
11
|
+
1. **Detects** your package manager, bundler, framework, and any stable
|
|
12
|
+
attributes already in use (`data-testid`, `data-cy`, …).
|
|
13
|
+
2. **Recommends** the hybrid locator strategy `source → attr → behavioral → dom`.
|
|
14
|
+
3. **Wires** the dev-only `@clicksmith/unplugin` into your Vite config (when the
|
|
15
|
+
stack supports it) so the agent gets exact `file:line` locators.
|
|
16
|
+
4. **Writes** agent instruction files for Claude (`CLAUDE.md`), Cursor
|
|
17
|
+
(`.cursor/rules/clicksmith.mdc`), Codex (`AGENTS.md`), and a generic agent —
|
|
18
|
+
using **managed blocks** so your existing content is preserved.
|
|
19
|
+
5. **Merges** `agents.config.json` (defaults + any project overrides) into
|
|
20
|
+
`.clicksmith/`.
|
|
21
|
+
6. **Registers** the daemon's MCP server in `.mcp.json` (and `.cursor/mcp.json`),
|
|
22
|
+
merging with any servers you already have.
|
|
23
|
+
7. **Gitignores** ClickSmith's runtime state (`.clicksmith/`).
|
|
24
|
+
|
|
25
|
+
## Flags
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
create-clicksmith [dir]
|
|
29
|
+
--dry-run # print the plan, write nothing
|
|
30
|
+
--no-unplugin # skip the data-loc plugin wiring
|
|
31
|
+
--agents claude,cursor # which instruction files to render
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Programmatic API
|
|
35
|
+
|
|
36
|
+
The CLI is a thin wrapper around testable functions:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { detectProject, planInstall, applyPlan } from 'create-clicksmith';
|
|
40
|
+
|
|
41
|
+
const info = await detectProject(process.cwd());
|
|
42
|
+
const plan = await planInstall(info, { useUnplugin: info.supportsUnplugin });
|
|
43
|
+
// inspect plan.changes (each has action: create | merge | skip) …
|
|
44
|
+
await applyPlan(process.cwd(), plan);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`planInstall` reads existing files and computes the *final merged contents* up
|
|
48
|
+
front, so it is fully deterministic and easy to test. `applyPlan` only writes.
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// src/detect.ts
|
|
2
|
+
import { readFile, readdir } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
var KNOWN_STABLE_ATTRS = ["data-testid", "data-test", "data-cy", "data-qa"];
|
|
5
|
+
async function detectProject(root) {
|
|
6
|
+
const pkg = await readJson(join(root, "package.json"));
|
|
7
|
+
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
8
|
+
const packageManager = await detectPackageManager(root);
|
|
9
|
+
const bundler = detectBundler(deps);
|
|
10
|
+
const framework = detectFramework(deps);
|
|
11
|
+
const viteConfig = await findFile(root, ["vite.config.ts", "vite.config.js", "vite.config.mjs"]);
|
|
12
|
+
const stableAttrs = await scanStableAttrs(root);
|
|
13
|
+
const supportsUnplugin = framework === "react" && ["vite", "webpack", "rspack", "rollup"].includes(bundler);
|
|
14
|
+
return {
|
|
15
|
+
root,
|
|
16
|
+
packageManager,
|
|
17
|
+
bundler,
|
|
18
|
+
framework,
|
|
19
|
+
stableAttrs,
|
|
20
|
+
supportsUnplugin,
|
|
21
|
+
...viteConfig ? { viteConfig } : {}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function detectPackageManager(root) {
|
|
25
|
+
if (await fileExists(join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
26
|
+
if (await fileExists(join(root, "yarn.lock"))) return "yarn";
|
|
27
|
+
if (await fileExists(join(root, "bun.lockb"))) return "bun";
|
|
28
|
+
return "npm";
|
|
29
|
+
}
|
|
30
|
+
function detectBundler(deps) {
|
|
31
|
+
if (deps.next) return "next";
|
|
32
|
+
if (deps.vite) return "vite";
|
|
33
|
+
if (deps["@rspack/core"]) return "rspack";
|
|
34
|
+
if (deps.webpack) return "webpack";
|
|
35
|
+
if (deps.rollup) return "rollup";
|
|
36
|
+
return "unknown";
|
|
37
|
+
}
|
|
38
|
+
function detectFramework(deps) {
|
|
39
|
+
if (deps["@angular/core"]) return "angular";
|
|
40
|
+
if (deps.svelte) return "svelte";
|
|
41
|
+
if (deps.vue) return "vue";
|
|
42
|
+
if (deps["solid-js"]) return "solid";
|
|
43
|
+
if (deps.react) return "react";
|
|
44
|
+
return "unknown";
|
|
45
|
+
}
|
|
46
|
+
async function scanStableAttrs(root) {
|
|
47
|
+
const found = /* @__PURE__ */ new Set();
|
|
48
|
+
const dirs = ["src", "app", "components"];
|
|
49
|
+
for (const dir of dirs) {
|
|
50
|
+
const files = await listSourceFiles(join(root, dir), 40);
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const content = await safeRead(file);
|
|
53
|
+
if (!content) continue;
|
|
54
|
+
for (const attr of KNOWN_STABLE_ATTRS) {
|
|
55
|
+
if (content.includes(attr)) found.add(attr);
|
|
56
|
+
}
|
|
57
|
+
if (found.size === KNOWN_STABLE_ATTRS.length) return [...found];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...found];
|
|
61
|
+
}
|
|
62
|
+
async function listSourceFiles(dir, limit) {
|
|
63
|
+
const out = [];
|
|
64
|
+
async function walk(d) {
|
|
65
|
+
if (out.length >= limit) return;
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
69
|
+
} catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (out.length >= limit) return;
|
|
74
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
75
|
+
const full = join(d, entry.name);
|
|
76
|
+
if (entry.isDirectory()) await walk(full);
|
|
77
|
+
else if (/\.(jsx?|tsx?|vue|svelte)$/.test(entry.name)) out.push(full);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
await walk(dir);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
async function findFile(root, names) {
|
|
84
|
+
for (const name of names) {
|
|
85
|
+
if (await fileExists(join(root, name))) return name;
|
|
86
|
+
}
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
async function readJson(file) {
|
|
90
|
+
const raw = await safeRead(file);
|
|
91
|
+
if (!raw) return void 0;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(raw);
|
|
94
|
+
} catch {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function safeRead(file) {
|
|
99
|
+
try {
|
|
100
|
+
return await readFile(file, "utf8");
|
|
101
|
+
} catch {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function fileExists(file) {
|
|
106
|
+
return await safeRead(file) !== void 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/install.ts
|
|
110
|
+
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
111
|
+
import { dirname, join as join2 } from "path";
|
|
112
|
+
import {
|
|
113
|
+
applyManagedBlock,
|
|
114
|
+
DEFAULT_AGENTS_CONFIG,
|
|
115
|
+
mergeAgentsConfig,
|
|
116
|
+
parseAgentsConfig,
|
|
117
|
+
renderInstructions
|
|
118
|
+
} from "@clicksmith/agent-config";
|
|
119
|
+
import { DEFAULT_DAEMON_PORT } from "@clicksmith/core";
|
|
120
|
+
var DEFAULT_AGENT_TARGETS = ["claude", "cursor", "codex", "generic"];
|
|
121
|
+
async function planInstall(info, options = {}) {
|
|
122
|
+
const root = info.root;
|
|
123
|
+
const targets = options.agents ?? DEFAULT_AGENT_TARGETS;
|
|
124
|
+
const useUnplugin = options.useUnplugin ?? info.supportsUnplugin;
|
|
125
|
+
const port = options.daemonPort ?? DEFAULT_DAEMON_PORT;
|
|
126
|
+
const changes = [];
|
|
127
|
+
const messages = [];
|
|
128
|
+
const nextSteps = [];
|
|
129
|
+
for (const target of targets) {
|
|
130
|
+
const rendered = renderInstructions(target, { stableAttrs: info.stableAttrs, daemonPort: port });
|
|
131
|
+
const existing = await readText(join2(root, rendered.path));
|
|
132
|
+
const contents = rendered.shared ? applyManagedBlock(existing, rendered.content) : rendered.content;
|
|
133
|
+
changes.push({
|
|
134
|
+
path: rendered.path,
|
|
135
|
+
action: existing == null ? "create" : "merge",
|
|
136
|
+
contents
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const configPath = join2(".clicksmith", "agents.config.json");
|
|
140
|
+
const existingConfigRaw = await readText(join2(root, configPath));
|
|
141
|
+
let merged = DEFAULT_AGENTS_CONFIG;
|
|
142
|
+
if (existingConfigRaw) {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = parseAgentsConfig(JSON.parse(existingConfigRaw));
|
|
145
|
+
if (parsed.ok) merged = mergeAgentsConfig(DEFAULT_AGENTS_CONFIG, parsed.config);
|
|
146
|
+
} catch {
|
|
147
|
+
messages.push("Existing agents.config.json was invalid JSON; left it untouched.");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
changes.push({
|
|
151
|
+
path: configPath,
|
|
152
|
+
action: existingConfigRaw == null ? "create" : "merge",
|
|
153
|
+
contents: `${JSON.stringify(merged, null, 2)}
|
|
154
|
+
`
|
|
155
|
+
});
|
|
156
|
+
const mcp = mcpCommand(info.packageManager);
|
|
157
|
+
changes.push(await mcpChange(root, ".mcp.json", mcp));
|
|
158
|
+
if (targets.includes("cursor")) {
|
|
159
|
+
changes.push(await mcpChange(root, join2(".cursor", "mcp.json"), mcp));
|
|
160
|
+
}
|
|
161
|
+
if (useUnplugin && info.viteConfig) {
|
|
162
|
+
const wired = await wireViteConfig(root, info.viteConfig);
|
|
163
|
+
if (wired) changes.push(wired);
|
|
164
|
+
else
|
|
165
|
+
messages.push(
|
|
166
|
+
`Could not automatically wire ${info.viteConfig}. Add: import clicksmith from '@clicksmith/unplugin/vite'; and put clicksmith() first in plugins.`
|
|
167
|
+
);
|
|
168
|
+
} else if (useUnplugin) {
|
|
169
|
+
messages.push("No Vite config found \u2014 add the @clicksmith/unplugin plugin to your bundler manually.");
|
|
170
|
+
} else {
|
|
171
|
+
messages.push(
|
|
172
|
+
`Stable source locators via unplugin aren't available for this stack; ClickSmith will use ${info.stableAttrs.length ? `your attributes (${info.stableAttrs.join(", ")})` : "attribute/behavioral/DOM"} as the fallback locator (source \u2192 attr \u2192 behavioral \u2192 dom).`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
changes.push(await packageJsonChange(root, useUnplugin));
|
|
176
|
+
changes.push(await gitignoreChange(root));
|
|
177
|
+
nextSteps.push(
|
|
178
|
+
`${installCmd(info.packageManager)} # install the new dependencies`,
|
|
179
|
+
`${runCmd(info.packageManager, "clicksmith daemon")} # start the localhost daemon`,
|
|
180
|
+
"Load the ClickSmith extension, toggle AI Mode, and Alt+Click an element."
|
|
181
|
+
);
|
|
182
|
+
return { changes, messages, nextSteps };
|
|
183
|
+
}
|
|
184
|
+
async function applyPlan(root, plan) {
|
|
185
|
+
const written = [];
|
|
186
|
+
for (const change of plan.changes) {
|
|
187
|
+
if (change.action === "skip") continue;
|
|
188
|
+
const full = join2(root, change.path);
|
|
189
|
+
await mkdir(dirname(full), { recursive: true });
|
|
190
|
+
await writeFile(full, change.contents, "utf8");
|
|
191
|
+
written.push(change);
|
|
192
|
+
}
|
|
193
|
+
return written;
|
|
194
|
+
}
|
|
195
|
+
function mcpCommand(pm) {
|
|
196
|
+
switch (pm) {
|
|
197
|
+
case "pnpm":
|
|
198
|
+
return { command: "pnpm", args: ["exec", "clicksmith", "mcp"] };
|
|
199
|
+
case "yarn":
|
|
200
|
+
return { command: "yarn", args: ["clicksmith", "mcp"] };
|
|
201
|
+
case "bun":
|
|
202
|
+
return { command: "bunx", args: ["clicksmith", "mcp"] };
|
|
203
|
+
case "npm":
|
|
204
|
+
return { command: "npx", args: ["clicksmith", "mcp"] };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function mcpChange(root, path, spec) {
|
|
208
|
+
const existingRaw = await readText(join2(root, path));
|
|
209
|
+
let doc = {};
|
|
210
|
+
if (existingRaw) {
|
|
211
|
+
try {
|
|
212
|
+
doc = JSON.parse(existingRaw);
|
|
213
|
+
} catch {
|
|
214
|
+
doc = {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
doc.mcpServers = { ...doc.mcpServers ?? {}, clicksmith: spec };
|
|
218
|
+
return {
|
|
219
|
+
path,
|
|
220
|
+
action: existingRaw == null ? "create" : "merge",
|
|
221
|
+
contents: `${JSON.stringify(doc, null, 2)}
|
|
222
|
+
`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async function wireViteConfig(root, relPath) {
|
|
226
|
+
const file = join2(root, relPath);
|
|
227
|
+
const code = await readText(file);
|
|
228
|
+
if (code == null) return null;
|
|
229
|
+
if (code.includes("@clicksmith/unplugin")) {
|
|
230
|
+
return { path: relPath, action: "skip", contents: code, reason: "already wired" };
|
|
231
|
+
}
|
|
232
|
+
const importLine = `import clicksmith from '@clicksmith/unplugin/vite';
|
|
233
|
+
`;
|
|
234
|
+
const pluginsMatch = code.match(/plugins\s*:\s*\[/);
|
|
235
|
+
if (!pluginsMatch) return null;
|
|
236
|
+
const idx = pluginsMatch.index + pluginsMatch[0].length;
|
|
237
|
+
const next = importLine + code.slice(0, idx) + "clicksmith(), " + code.slice(idx);
|
|
238
|
+
return { path: relPath, action: "merge", contents: next };
|
|
239
|
+
}
|
|
240
|
+
async function packageJsonChange(root, useUnplugin) {
|
|
241
|
+
const raw = await readText(join2(root, "package.json"));
|
|
242
|
+
const pkg = raw ? JSON.parse(raw) : { name: "project", version: "0.0.0" };
|
|
243
|
+
pkg.devDependencies = { ...pkg.devDependencies ?? {} };
|
|
244
|
+
pkg.devDependencies["@clicksmith/daemon"] = pkg.devDependencies["@clicksmith/daemon"] ?? "latest";
|
|
245
|
+
if (useUnplugin) {
|
|
246
|
+
pkg.devDependencies["@clicksmith/unplugin"] = pkg.devDependencies["@clicksmith/unplugin"] ?? "latest";
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
path: "package.json",
|
|
250
|
+
action: raw == null ? "create" : "merge",
|
|
251
|
+
contents: `${JSON.stringify(pkg, null, 2)}
|
|
252
|
+
`
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function gitignoreChange(root) {
|
|
256
|
+
const existing = await readText(join2(root, ".gitignore"));
|
|
257
|
+
if (existing?.split(/\r?\n/).some((l) => l.trim() === ".clicksmith/" || l.trim() === ".clicksmith")) {
|
|
258
|
+
return { path: ".gitignore", action: "skip", contents: existing };
|
|
259
|
+
}
|
|
260
|
+
const contents = existing ? `${existing.trimEnd()}
|
|
261
|
+
|
|
262
|
+
# ClickSmith runtime state
|
|
263
|
+
.clicksmith/
|
|
264
|
+
` : "# ClickSmith runtime state\n.clicksmith/\n";
|
|
265
|
+
return { path: ".gitignore", action: existing == null ? "create" : "merge", contents };
|
|
266
|
+
}
|
|
267
|
+
function installCmd(pm) {
|
|
268
|
+
return pm === "npm" ? "npm install" : `${pm} install`;
|
|
269
|
+
}
|
|
270
|
+
function runCmd(pm, script) {
|
|
271
|
+
switch (pm) {
|
|
272
|
+
case "pnpm":
|
|
273
|
+
return `pnpm exec ${script}`;
|
|
274
|
+
case "yarn":
|
|
275
|
+
return `yarn ${script}`;
|
|
276
|
+
case "bun":
|
|
277
|
+
return `bunx ${script}`;
|
|
278
|
+
case "npm":
|
|
279
|
+
return `npx ${script}`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function readText(file) {
|
|
283
|
+
try {
|
|
284
|
+
return await readFile2(file, "utf8");
|
|
285
|
+
} catch {
|
|
286
|
+
return void 0;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export {
|
|
291
|
+
detectProject,
|
|
292
|
+
planInstall,
|
|
293
|
+
applyPlan
|
|
294
|
+
};
|
|
295
|
+
//# sourceMappingURL=chunk-WEMLICDQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/detect.ts","../src/install.ts"],"sourcesContent":["import { readFile, readdir } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm';\nexport type Bundler = 'vite' | 'webpack' | 'rspack' | 'rollup' | 'next' | 'unknown';\nexport type Framework = 'react' | 'vue' | 'svelte' | 'angular' | 'solid' | 'unknown';\n\nexport interface ProjectInfo {\n root: string;\n packageManager: PackageManager;\n bundler: Bundler;\n framework: Framework;\n /** Stable attributes already used in the codebase (e.g. data-testid). */\n stableAttrs: string[];\n /** Whether the dev-only data-loc unplugin can be wired up for this stack. */\n supportsUnplugin: boolean;\n /** Path to the detected Vite config, if any (relative to root). */\n viteConfig?: string;\n}\n\nconst KNOWN_STABLE_ATTRS = ['data-testid', 'data-test', 'data-cy', 'data-qa'];\n\n/** Inspect a project directory and infer its stack. Read-only. */\nexport async function detectProject(root: string): Promise<ProjectInfo> {\n const pkg = await readJson(join(root, 'package.json'));\n const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) } as Record<string, string>;\n\n const packageManager = await detectPackageManager(root);\n const bundler = detectBundler(deps);\n const framework = detectFramework(deps);\n const viteConfig = await findFile(root, ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']);\n const stableAttrs = await scanStableAttrs(root);\n\n const supportsUnplugin =\n framework === 'react' && ['vite', 'webpack', 'rspack', 'rollup'].includes(bundler);\n\n return {\n root,\n packageManager,\n bundler,\n framework,\n stableAttrs,\n supportsUnplugin,\n ...(viteConfig ? { viteConfig } : {}),\n };\n}\n\nasync function detectPackageManager(root: string): Promise<PackageManager> {\n if (await fileExists(join(root, 'pnpm-lock.yaml'))) return 'pnpm';\n if (await fileExists(join(root, 'yarn.lock'))) return 'yarn';\n if (await fileExists(join(root, 'bun.lockb'))) return 'bun';\n return 'npm';\n}\n\nfunction detectBundler(deps: Record<string, string>): Bundler {\n if (deps.next) return 'next';\n if (deps.vite) return 'vite';\n if (deps['@rspack/core']) return 'rspack';\n if (deps.webpack) return 'webpack';\n if (deps.rollup) return 'rollup';\n return 'unknown';\n}\n\nfunction detectFramework(deps: Record<string, string>): Framework {\n if (deps['@angular/core']) return 'angular';\n if (deps.svelte) return 'svelte';\n if (deps.vue) return 'vue';\n if (deps['solid-js']) return 'solid';\n if (deps.react) return 'react';\n return 'unknown';\n}\n\n/** Scan a handful of source files for stable test/id attributes. */\nasync function scanStableAttrs(root: string): Promise<string[]> {\n const found = new Set<string>();\n const dirs = ['src', 'app', 'components'];\n for (const dir of dirs) {\n const files = await listSourceFiles(join(root, dir), 40);\n for (const file of files) {\n const content = await safeRead(file);\n if (!content) continue;\n for (const attr of KNOWN_STABLE_ATTRS) {\n if (content.includes(attr)) found.add(attr);\n }\n if (found.size === KNOWN_STABLE_ATTRS.length) return [...found];\n }\n }\n return [...found];\n}\n\nasync function listSourceFiles(dir: string, limit: number): Promise<string[]> {\n const out: string[] = [];\n async function walk(d: string) {\n if (out.length >= limit) return;\n let entries;\n try {\n entries = await readdir(d, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (out.length >= limit) return;\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n const full = join(d, entry.name);\n if (entry.isDirectory()) await walk(full);\n else if (/\\.(jsx?|tsx?|vue|svelte)$/.test(entry.name)) out.push(full);\n }\n }\n await walk(dir);\n return out;\n}\n\nasync function findFile(root: string, names: string[]): Promise<string | undefined> {\n for (const name of names) {\n if (await fileExists(join(root, name))) return name;\n }\n return undefined;\n}\n\nasync function readJson(file: string): Promise<Record<string, unknown> | undefined> {\n const raw = await safeRead(file);\n if (!raw) return undefined;\n try {\n return JSON.parse(raw);\n } catch {\n return undefined;\n }\n}\n\nasync function safeRead(file: string): Promise<string | undefined> {\n try {\n return await readFile(file, 'utf8');\n } catch {\n return undefined;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n return (await safeRead(file)) !== undefined;\n}\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport {\n applyManagedBlock,\n DEFAULT_AGENTS_CONFIG,\n mergeAgentsConfig,\n parseAgentsConfig,\n renderInstructions,\n type InstructionTarget,\n} from '@clicksmith/agent-config';\nimport { DEFAULT_DAEMON_PORT } from '@clicksmith/core';\nimport type { PackageManager, ProjectInfo } from './detect.js';\n\nexport interface InstallOptions {\n /** Which agents to render instruction files for. */\n agents?: InstructionTarget[];\n /** Wire the dev-only data-loc unplugin. Defaults to detection. */\n useUnplugin?: boolean;\n daemonPort?: number;\n}\n\nexport interface FileChange {\n path: string;\n action: 'create' | 'merge' | 'skip';\n contents: string;\n reason?: string;\n}\n\nexport interface InstallPlan {\n changes: FileChange[];\n messages: string[];\n nextSteps: string[];\n}\n\nconst DEFAULT_AGENT_TARGETS: InstructionTarget[] = ['claude', 'cursor', 'codex', 'generic'];\n\n/**\n * Compute every file change needed to install ClickSmith into a project,\n * reading existing files so merges preserve user content. Nothing is written —\n * call {@link applyPlan} for that. This split keeps the installer fully testable.\n */\nexport async function planInstall(info: ProjectInfo, options: InstallOptions = {}): Promise<InstallPlan> {\n const root = info.root;\n const targets = options.agents ?? DEFAULT_AGENT_TARGETS;\n const useUnplugin = options.useUnplugin ?? info.supportsUnplugin;\n const port = options.daemonPort ?? DEFAULT_DAEMON_PORT;\n\n const changes: FileChange[] = [];\n const messages: string[] = [];\n const nextSteps: string[] = [];\n\n // 1. Agent instruction files (managed blocks preserve user content).\n for (const target of targets) {\n const rendered = renderInstructions(target, { stableAttrs: info.stableAttrs, daemonPort: port });\n const existing = await readText(join(root, rendered.path));\n const contents = rendered.shared\n ? applyManagedBlock(existing, rendered.content)\n : rendered.content;\n changes.push({\n path: rendered.path,\n action: existing == null ? 'create' : 'merge',\n contents,\n });\n }\n\n // 2. agents.config.json — merge defaults with any existing project config.\n const configPath = join('.clicksmith', 'agents.config.json');\n const existingConfigRaw = await readText(join(root, configPath));\n let merged = DEFAULT_AGENTS_CONFIG;\n if (existingConfigRaw) {\n try {\n const parsed = parseAgentsConfig(JSON.parse(existingConfigRaw));\n if (parsed.ok) merged = mergeAgentsConfig(DEFAULT_AGENTS_CONFIG, parsed.config);\n } catch {\n messages.push('Existing agents.config.json was invalid JSON; left it untouched.');\n }\n }\n changes.push({\n path: configPath,\n action: existingConfigRaw == null ? 'create' : 'merge',\n contents: `${JSON.stringify(merged, null, 2)}\\n`,\n });\n\n // 3. MCP registration for the daemon's stdio server.\n const mcp = mcpCommand(info.packageManager);\n changes.push(await mcpChange(root, '.mcp.json', mcp));\n if (targets.includes('cursor')) {\n changes.push(await mcpChange(root, join('.cursor', 'mcp.json'), mcp));\n }\n\n // 4. Wire the data-loc unplugin (best effort) + record the dependency.\n if (useUnplugin && info.viteConfig) {\n const wired = await wireViteConfig(root, info.viteConfig);\n if (wired) changes.push(wired);\n else\n messages.push(\n `Could not automatically wire ${info.viteConfig}. Add: import clicksmith from '@clicksmith/unplugin/vite'; and put clicksmith() first in plugins.`,\n );\n } else if (useUnplugin) {\n messages.push('No Vite config found — add the @clicksmith/unplugin plugin to your bundler manually.');\n } else {\n messages.push(\n `Stable source locators via unplugin aren't available for this stack; ClickSmith will use ${\n info.stableAttrs.length ? `your attributes (${info.stableAttrs.join(', ')})` : 'attribute/behavioral/DOM'\n } as the fallback locator (source → attr → behavioral → dom).`,\n );\n }\n\n // 5. package.json — add the dependencies needed for the bin + plugin.\n changes.push(await packageJsonChange(root, useUnplugin));\n\n // 6. Ensure .clicksmith/ is gitignored.\n changes.push(await gitignoreChange(root));\n\n nextSteps.push(\n `${installCmd(info.packageManager)} # install the new dependencies`,\n `${runCmd(info.packageManager, 'clicksmith daemon')} # start the localhost daemon`,\n 'Load the ClickSmith extension, toggle AI Mode, and Alt+Click an element.',\n );\n\n return { changes, messages, nextSteps };\n}\n\n/** Apply a plan to disk, writing create/merge changes and skipping the rest. */\nexport async function applyPlan(root: string, plan: InstallPlan): Promise<FileChange[]> {\n const written: FileChange[] = [];\n for (const change of plan.changes) {\n if (change.action === 'skip') continue;\n const full = join(root, change.path);\n await mkdir(dirname(full), { recursive: true });\n await writeFile(full, change.contents, 'utf8');\n written.push(change);\n }\n return written;\n}\n\n/* ------------------------------- helpers ---------------------------------- */\n\ninterface McpServerSpec {\n command: string;\n args: string[];\n}\n\nfunction mcpCommand(pm: PackageManager): McpServerSpec {\n switch (pm) {\n case 'pnpm':\n return { command: 'pnpm', args: ['exec', 'clicksmith', 'mcp'] };\n case 'yarn':\n return { command: 'yarn', args: ['clicksmith', 'mcp'] };\n case 'bun':\n return { command: 'bunx', args: ['clicksmith', 'mcp'] };\n case 'npm':\n return { command: 'npx', args: ['clicksmith', 'mcp'] };\n }\n}\n\nasync function mcpChange(root: string, path: string, spec: McpServerSpec): Promise<FileChange> {\n const existingRaw = await readText(join(root, path));\n let doc: { mcpServers?: Record<string, unknown> } = {};\n if (existingRaw) {\n try {\n doc = JSON.parse(existingRaw);\n } catch {\n doc = {};\n }\n }\n doc.mcpServers = { ...(doc.mcpServers ?? {}), clicksmith: spec };\n return {\n path,\n action: existingRaw == null ? 'create' : 'merge',\n contents: `${JSON.stringify(doc, null, 2)}\\n`,\n };\n}\n\nasync function wireViteConfig(root: string, relPath: string): Promise<FileChange | null> {\n const file = join(root, relPath);\n const code = await readText(file);\n if (code == null) return null;\n if (code.includes('@clicksmith/unplugin')) {\n return { path: relPath, action: 'skip', contents: code, reason: 'already wired' };\n }\n const importLine = `import clicksmith from '@clicksmith/unplugin/vite';\\n`;\n // Insert plugin first in the array so it runs before the framework plugin.\n const pluginsMatch = code.match(/plugins\\s*:\\s*\\[/);\n if (!pluginsMatch) return null;\n const idx = pluginsMatch.index! + pluginsMatch[0].length;\n const next = importLine + code.slice(0, idx) + 'clicksmith(), ' + code.slice(idx);\n return { path: relPath, action: 'merge', contents: next };\n}\n\nasync function packageJsonChange(root: string, useUnplugin: boolean): Promise<FileChange> {\n const raw = await readText(join(root, 'package.json'));\n const pkg = raw ? JSON.parse(raw) : { name: 'project', version: '0.0.0' };\n pkg.devDependencies = { ...(pkg.devDependencies ?? {}) };\n pkg.devDependencies['@clicksmith/daemon'] = pkg.devDependencies['@clicksmith/daemon'] ?? 'latest';\n if (useUnplugin) {\n pkg.devDependencies['@clicksmith/unplugin'] = pkg.devDependencies['@clicksmith/unplugin'] ?? 'latest';\n }\n return {\n path: 'package.json',\n action: raw == null ? 'create' : 'merge',\n contents: `${JSON.stringify(pkg, null, 2)}\\n`,\n };\n}\n\nasync function gitignoreChange(root: string): Promise<FileChange> {\n const existing = await readText(join(root, '.gitignore'));\n if (existing?.split(/\\r?\\n/).some((l) => l.trim() === '.clicksmith/' || l.trim() === '.clicksmith')) {\n return { path: '.gitignore', action: 'skip', contents: existing };\n }\n const contents = existing\n ? `${existing.trimEnd()}\\n\\n# ClickSmith runtime state\\n.clicksmith/\\n`\n : '# ClickSmith runtime state\\n.clicksmith/\\n';\n return { path: '.gitignore', action: existing == null ? 'create' : 'merge', contents };\n}\n\nfunction installCmd(pm: PackageManager): string {\n return pm === 'npm' ? 'npm install' : `${pm} install`;\n}\n\nfunction runCmd(pm: PackageManager, script: string): string {\n switch (pm) {\n case 'pnpm':\n return `pnpm exec ${script}`;\n case 'yarn':\n return `yarn ${script}`;\n case 'bun':\n return `bunx ${script}`;\n case 'npm':\n return `npx ${script}`;\n }\n}\n\nasync function readText(file: string): Promise<string | undefined> {\n try {\n return await readFile(file, 'utf8');\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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","names":["readFile","join"]}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
applyPlan,
|
|
4
|
+
detectProject,
|
|
5
|
+
planInstall
|
|
6
|
+
} from "./chunk-WEMLICDQ.js";
|
|
7
|
+
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
var HELP = `create-clicksmith
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
create-clicksmith [dir] [--dry-run] [--no-unplugin] [--agents claude,cursor,codex,generic]
|
|
13
|
+
|
|
14
|
+
Detects your stack, wires stable locators, writes agent instruction files,
|
|
15
|
+
merges agents.config.json, and registers the ClickSmith MCP server. Existing
|
|
16
|
+
files are preserved (managed blocks / JSON merges).
|
|
17
|
+
`;
|
|
18
|
+
async function main() {
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
21
|
+
process.stdout.write(HELP);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const positional = args.find((a) => !a.startsWith("--"));
|
|
25
|
+
const root = positional ? resolveDir(positional) : process.cwd();
|
|
26
|
+
const dryRun = args.includes("--dry-run");
|
|
27
|
+
const useUnplugin = !args.includes("--no-unplugin");
|
|
28
|
+
const agentsArg = valueOf(args, "--agents");
|
|
29
|
+
const agents = agentsArg ? agentsArg.split(",") : void 0;
|
|
30
|
+
log(`\u2692\uFE0F ClickSmith installer
|
|
31
|
+
`);
|
|
32
|
+
const info = await detectProject(root);
|
|
33
|
+
log(`Detected:`);
|
|
34
|
+
log(` package manager : ${info.packageManager}`);
|
|
35
|
+
log(` bundler : ${info.bundler}`);
|
|
36
|
+
log(` framework : ${info.framework}`);
|
|
37
|
+
log(` stable attrs : ${info.stableAttrs.join(", ") || "(none found)"}`);
|
|
38
|
+
log(` unplugin : ${info.supportsUnplugin ? "supported" : "not available for this stack"}`);
|
|
39
|
+
log("");
|
|
40
|
+
const plan = await planInstall(info, {
|
|
41
|
+
...agents ? { agents } : {},
|
|
42
|
+
useUnplugin: useUnplugin && info.supportsUnplugin
|
|
43
|
+
});
|
|
44
|
+
log(`Planned changes:`);
|
|
45
|
+
for (const change of plan.changes) {
|
|
46
|
+
const mark = change.action === "create" ? "+" : change.action === "merge" ? "~" : "\xB7";
|
|
47
|
+
log(` ${mark} ${change.path}${change.action === "skip" ? ` (${change.reason ?? "unchanged"})` : ""}`);
|
|
48
|
+
}
|
|
49
|
+
log("");
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
log("--dry-run: no files written.");
|
|
52
|
+
} else {
|
|
53
|
+
const written = await applyPlan(root, plan);
|
|
54
|
+
log(`Wrote ${written.length} file(s).`);
|
|
55
|
+
}
|
|
56
|
+
for (const message of plan.messages) log(`\u2139\uFE0F ${message}`);
|
|
57
|
+
log("\nNext steps:");
|
|
58
|
+
for (const step of plan.nextSteps) log(` ${step}`);
|
|
59
|
+
}
|
|
60
|
+
function resolveDir(dir) {
|
|
61
|
+
return dir.startsWith("/") ? dir : `${process.cwd()}/${dir}`;
|
|
62
|
+
}
|
|
63
|
+
function valueOf(args, flag) {
|
|
64
|
+
const i = args.indexOf(flag);
|
|
65
|
+
return i !== -1 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : void 0;
|
|
66
|
+
}
|
|
67
|
+
function log(line) {
|
|
68
|
+
process.stdout.write(`${line}
|
|
69
|
+
`);
|
|
70
|
+
}
|
|
71
|
+
main().catch((err) => {
|
|
72
|
+
process.stderr.write(`create-clicksmith failed: ${err instanceof Error ? err.stack : String(err)}
|
|
73
|
+
`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { detectProject } from './detect.js';\nimport { applyPlan, planInstall } from './install.js';\n\nconst HELP = `create-clicksmith\n\nUsage:\n create-clicksmith [dir] [--dry-run] [--no-unplugin] [--agents claude,cursor,codex,generic]\n\nDetects your stack, wires stable locators, writes agent instruction files,\nmerges agents.config.json, and registers the ClickSmith MCP server. Existing\nfiles are preserved (managed blocks / JSON merges).\n`;\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args.includes('--help') || args.includes('-h')) {\n process.stdout.write(HELP);\n return;\n }\n\n const positional = args.find((a) => !a.startsWith('--'));\n const root = positional ? resolveDir(positional) : process.cwd();\n const dryRun = args.includes('--dry-run');\n const useUnplugin = !args.includes('--no-unplugin');\n const agentsArg = valueOf(args, '--agents');\n const agents = agentsArg ? (agentsArg.split(',') as never) : undefined;\n\n log(`⚒️ ClickSmith installer\\n`);\n const info = await detectProject(root);\n log(`Detected:`);\n log(` package manager : ${info.packageManager}`);\n log(` bundler : ${info.bundler}`);\n log(` framework : ${info.framework}`);\n log(` stable attrs : ${info.stableAttrs.join(', ') || '(none found)'}`);\n log(` unplugin : ${info.supportsUnplugin ? 'supported' : 'not available for this stack'}`);\n log('');\n\n const plan = await planInstall(info, {\n ...(agents ? { agents } : {}),\n useUnplugin: useUnplugin && info.supportsUnplugin,\n });\n\n log(`Planned changes:`);\n for (const change of plan.changes) {\n const mark = change.action === 'create' ? '+' : change.action === 'merge' ? '~' : '·';\n log(` ${mark} ${change.path}${change.action === 'skip' ? ` (${change.reason ?? 'unchanged'})` : ''}`);\n }\n log('');\n\n if (dryRun) {\n log('--dry-run: no files written.');\n } else {\n const written = await applyPlan(root, plan);\n log(`Wrote ${written.length} file(s).`);\n }\n\n for (const message of plan.messages) log(`ℹ️ ${message}`);\n log('\\nNext steps:');\n for (const step of plan.nextSteps) log(` ${step}`);\n}\n\nfunction resolveDir(dir: string): string {\n return dir.startsWith('/') ? dir : `${process.cwd()}/${dir}`;\n}\n\nfunction valueOf(args: string[], flag: string): string | undefined {\n const i = args.indexOf(flag);\n return i !== -1 && args[i + 1] && !args[i + 1]!.startsWith('--') ? args[i + 1] : undefined;\n}\n\nfunction log(line: string): void {\n process.stdout.write(`${line}\\n`);\n}\n\nmain().catch((err) => {\n process.stderr.write(`create-clicksmith failed: ${err instanceof Error ? err.stack : String(err)}\\n`);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;AAIA,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUb,eAAe,OAAsB;AACnC,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,MAAI,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,IAAI,GAAG;AAClD,YAAQ,OAAO,MAAM,IAAI;AACzB;AAAA,EACF;AAEA,QAAM,aAAa,KAAK,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,IAAI,CAAC;AACvD,QAAM,OAAO,aAAa,WAAW,UAAU,IAAI,QAAQ,IAAI;AAC/D,QAAM,SAAS,KAAK,SAAS,WAAW;AACxC,QAAM,cAAc,CAAC,KAAK,SAAS,eAAe;AAClD,QAAM,YAAY,QAAQ,MAAM,UAAU;AAC1C,QAAM,SAAS,YAAa,UAAU,MAAM,GAAG,IAAc;AAE7D,MAAI;AAAA,CAA4B;AAChC,QAAM,OAAO,MAAM,cAAc,IAAI;AACrC,MAAI,WAAW;AACf,MAAI,uBAAuB,KAAK,cAAc,EAAE;AAChD,MAAI,uBAAuB,KAAK,OAAO,EAAE;AACzC,MAAI,uBAAuB,KAAK,SAAS,EAAE;AAC3C,MAAI,uBAAuB,KAAK,YAAY,KAAK,IAAI,KAAK,cAAc,EAAE;AAC1E,MAAI,uBAAuB,KAAK,mBAAmB,cAAc,8BAA8B,EAAE;AACjG,MAAI,EAAE;AAEN,QAAM,OAAO,MAAM,YAAY,MAAM;AAAA,IACnC,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,aAAa,eAAe,KAAK;AAAA,EACnC,CAAC;AAED,MAAI,kBAAkB;AACtB,aAAW,UAAU,KAAK,SAAS;AACjC,UAAM,OAAO,OAAO,WAAW,WAAW,MAAM,OAAO,WAAW,UAAU,MAAM;AAClF,QAAI,KAAK,IAAI,IAAI,OAAO,IAAI,GAAG,OAAO,WAAW,SAAS,KAAK,OAAO,UAAU,WAAW,MAAM,EAAE,EAAE;AAAA,EACvG;AACA,MAAI,EAAE;AAEN,MAAI,QAAQ;AACV,QAAI,8BAA8B;AAAA,EACpC,OAAO;AACL,UAAM,UAAU,MAAM,UAAU,MAAM,IAAI;AAC1C,QAAI,SAAS,QAAQ,MAAM,WAAW;AAAA,EACxC;AAEA,aAAW,WAAW,KAAK,SAAU,KAAI,iBAAO,OAAO,EAAE;AACzD,MAAI,eAAe;AACnB,aAAW,QAAQ,KAAK,UAAW,KAAI,KAAK,IAAI,EAAE;AACpD;AAEA,SAAS,WAAW,KAAqB;AACvC,SAAO,IAAI,WAAW,GAAG,IAAI,MAAM,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG;AAC5D;AAEA,SAAS,QAAQ,MAAgB,MAAkC;AACjE,QAAM,IAAI,KAAK,QAAQ,IAAI;AAC3B,SAAO,MAAM,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,EAAG,WAAW,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI;AACnF;AAEA,SAAS,IAAI,MAAoB;AAC/B,UAAQ,OAAO,MAAM,GAAG,IAAI;AAAA,CAAI;AAClC;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,OAAO,MAAM,6BAA6B,eAAe,QAAQ,IAAI,QAAQ,OAAO,GAAG,CAAC;AAAA,CAAI;AACpG,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { InstructionTarget } from '@clicksmith/agent-config';
|
|
2
|
+
|
|
3
|
+
type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm';
|
|
4
|
+
type Bundler = 'vite' | 'webpack' | 'rspack' | 'rollup' | 'next' | 'unknown';
|
|
5
|
+
type Framework = 'react' | 'vue' | 'svelte' | 'angular' | 'solid' | 'unknown';
|
|
6
|
+
interface ProjectInfo {
|
|
7
|
+
root: string;
|
|
8
|
+
packageManager: PackageManager;
|
|
9
|
+
bundler: Bundler;
|
|
10
|
+
framework: Framework;
|
|
11
|
+
/** Stable attributes already used in the codebase (e.g. data-testid). */
|
|
12
|
+
stableAttrs: string[];
|
|
13
|
+
/** Whether the dev-only data-loc unplugin can be wired up for this stack. */
|
|
14
|
+
supportsUnplugin: boolean;
|
|
15
|
+
/** Path to the detected Vite config, if any (relative to root). */
|
|
16
|
+
viteConfig?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Inspect a project directory and infer its stack. Read-only. */
|
|
19
|
+
declare function detectProject(root: string): Promise<ProjectInfo>;
|
|
20
|
+
|
|
21
|
+
interface InstallOptions {
|
|
22
|
+
/** Which agents to render instruction files for. */
|
|
23
|
+
agents?: InstructionTarget[];
|
|
24
|
+
/** Wire the dev-only data-loc unplugin. Defaults to detection. */
|
|
25
|
+
useUnplugin?: boolean;
|
|
26
|
+
daemonPort?: number;
|
|
27
|
+
}
|
|
28
|
+
interface FileChange {
|
|
29
|
+
path: string;
|
|
30
|
+
action: 'create' | 'merge' | 'skip';
|
|
31
|
+
contents: string;
|
|
32
|
+
reason?: string;
|
|
33
|
+
}
|
|
34
|
+
interface InstallPlan {
|
|
35
|
+
changes: FileChange[];
|
|
36
|
+
messages: string[];
|
|
37
|
+
nextSteps: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compute every file change needed to install ClickSmith into a project,
|
|
41
|
+
* reading existing files so merges preserve user content. Nothing is written —
|
|
42
|
+
* call {@link applyPlan} for that. This split keeps the installer fully testable.
|
|
43
|
+
*/
|
|
44
|
+
declare function planInstall(info: ProjectInfo, options?: InstallOptions): Promise<InstallPlan>;
|
|
45
|
+
/** Apply a plan to disk, writing create/merge changes and skipping the rest. */
|
|
46
|
+
declare function applyPlan(root: string, plan: InstallPlan): Promise<FileChange[]>;
|
|
47
|
+
|
|
48
|
+
export { type Bundler, type FileChange, type Framework, type InstallOptions, type InstallPlan, type PackageManager, type ProjectInfo, applyPlan, detectProject, planInstall };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-clicksmith",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Project installer for ClickSmith: detect your stack, wire stable locators, write agent instructions, merge agents.config.json, and register the MCP server.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"bin": {
|
|
16
|
+
"create-clicksmith": "./dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@clicksmith/agent-config": "0.1.0",
|
|
23
|
+
"@clicksmith/core": "0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint src",
|
|
32
|
+
"clean": "rimraf dist .turbo"
|
|
33
|
+
}
|
|
34
|
+
}
|