4runr-cursor-setup 0.1.5 → 0.1.7

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/dist/cli.js CHANGED
@@ -1,51 +1,59 @@
1
1
  #!/usr/bin/env node
2
- import { init } from "./commands/init.js";
3
- import { addGroup, listGroups, removeGroup, GROUPS } from "./commands/commands.js";
4
- const args = process.argv.slice(2);
5
- const cmd = args[0];
2
+ import { listGroups, addGroup, removeGroup, GROUPS } from "./commands/commands.js";
3
+ import { doctor } from "./commands/doctor.js";
4
+ import { createRequire } from "module";
5
+ const require = createRequire(import.meta.url);
6
+ const TOOL_VERSION = require("../package.json").version;
6
7
  function usage() {
7
- console.log("4runr-cursor-setup");
8
8
  console.log("Usage:");
9
- console.log(" 4runr-cursor-setup init [--force]");
10
- console.log(" 4runr-cursor-setup list");
11
- console.log(" 4runr-cursor-setup add <group> [--force]");
12
- console.log(" 4runr-cursor-setup remove <group>");
9
+ console.log(" list");
10
+ console.log(" add <group> [--force] [--dry-run]");
11
+ console.log(" remove <group> [--dry-run]");
12
+ console.log(" doctor [--fix] [--strict] [--json] [--dry-run]");
13
13
  console.log("");
14
14
  console.log("Groups: " + Object.keys(GROUPS).join(", "));
15
15
  }
16
- async function main() {
17
- const force = args.includes("--force");
18
- if (cmd === "init") {
19
- await init({ cwd: process.cwd() });
20
- addGroup({ cwd: process.cwd(), group: "core", force });
21
- return;
22
- }
23
- if (cmd === "list") {
24
- listGroups();
25
- return;
26
- }
27
- if (cmd === "add") {
28
- const group = args[1];
29
- if (!group || !(group in GROUPS)) {
30
- usage();
31
- process.exit(1);
32
- }
33
- addGroup({ cwd: process.cwd(), group, force });
34
- return;
35
- }
36
- if (cmd === "remove") {
37
- const group = args[1];
38
- if (!group || !(group in GROUPS)) {
39
- usage();
40
- process.exit(1);
41
- }
42
- removeGroup({ cwd: process.cwd(), group });
43
- return;
44
- }
45
- usage();
46
- process.exit(1);
16
+ const args = process.argv.slice(2);
17
+ const cmd = args[0];
18
+ const cwd = process.cwd();
19
+ const flags = new Set(args.slice(1));
20
+ const has = (f) => flags.has(f);
21
+ function parseGroup(raw) {
22
+ if (!raw)
23
+ throw new Error("Missing group name");
24
+ const g = raw;
25
+ if (!(g in GROUPS))
26
+ throw new Error(`Unknown group: ${raw}`);
27
+ return g;
28
+ }
29
+ if (cmd === "list") {
30
+ listGroups();
31
+ process.exit(0);
32
+ }
33
+ if (cmd === "add") {
34
+ const group = parseGroup(args[1]);
35
+ addGroup({
36
+ cwd,
37
+ group,
38
+ force: has("--force"),
39
+ dryRun: has("--dry-run"),
40
+ });
41
+ process.exit(0);
42
+ }
43
+ if (cmd === "remove") {
44
+ const group = parseGroup(args[1]);
45
+ removeGroup({ cwd, group, dryRun: has("--dry-run") });
46
+ process.exit(0);
47
+ }
48
+ if (cmd === "doctor") {
49
+ doctor(cwd, TOOL_VERSION, {
50
+ fix: has("--fix"),
51
+ json: has("--json"),
52
+ strict: has("--strict"),
53
+ dryRun: has("--dry-run"),
54
+ });
55
+ // Exit with the exit code set by doctor (or 0 if not set)
56
+ process.exit(process.exitCode || 0);
47
57
  }
48
- main().catch((err) => {
49
- console.error(err);
50
- process.exit(1);
51
- });
58
+ usage();
59
+ process.exit(1);
@@ -1,13 +1,23 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import * as url from "url";
4
+ import { createRequire } from "module";
4
5
  import { ensureMemory } from "./memory.js";
6
+ import { loadManifest, saveManifest, createEmptyManifest, upsertFile, removeFile, sha256, } from "./manifest.js";
5
7
  export const GROUPS = {
6
8
  core: ["4runr-start.md", "4runr-close.md"],
7
9
  planning: ["4runr-task.md", "4runr-phase.md"],
8
10
  governance: ["4runr-decision.md", "4runr-scope-change.md"],
9
11
  debugging: ["4runr-repro.md", "4runr-verify.md"],
10
12
  };
13
+ function assertSafeRelPath(rel) {
14
+ const norm = rel.replace(/\\/g, "/");
15
+ if (norm.includes(".."))
16
+ throw new Error("Refusing unsafe path traversal: " + rel);
17
+ if (path.isAbsolute(rel))
18
+ throw new Error("Refusing absolute path: " + rel);
19
+ return norm;
20
+ }
11
21
  const MANAGED_MARKER = "<!-- managed-by: 4runr-cursor-setup -->";
12
22
  function ensureDir(p) {
13
23
  fs.mkdirSync(p, { recursive: true });
@@ -46,6 +56,29 @@ function getTemplatesRoot() {
46
56
  const here = path.dirname(url.fileURLToPath(import.meta.url));
47
57
  return path.resolve(here, "..", "templates");
48
58
  }
59
+ function getToolVersion() {
60
+ const require = createRequire(import.meta.url);
61
+ return require("../package.json").version;
62
+ }
63
+ function ensureManifest(cwd) {
64
+ let m = loadManifest(cwd);
65
+ if (!m) {
66
+ m = createEmptyManifest(getToolVersion());
67
+ saveManifest(cwd, m);
68
+ }
69
+ return m;
70
+ }
71
+ function assertPathUnderCommands(cwd, filePath) {
72
+ const resolved = path.resolve(cwd, filePath);
73
+ const commandsDir = path.resolve(cwd, ".cursor", "commands");
74
+ if (!resolved.startsWith(commandsDir + path.sep) && resolved !== commandsDir) {
75
+ throw new Error(`Path escapes .cursor/commands: ${filePath}`);
76
+ }
77
+ }
78
+ function getRelativePath(cwd, absolutePath) {
79
+ const rel = path.relative(cwd, absolutePath);
80
+ return assertSafeRelPath(rel);
81
+ }
49
82
  export function listGroups() {
50
83
  console.log("Available groups:");
51
84
  Object.keys(GROUPS).forEach((g) => {
@@ -53,42 +86,217 @@ export function listGroups() {
53
86
  });
54
87
  }
55
88
  export function addGroup(opts) {
56
- const { cwd, group, force = false } = opts;
89
+ const { cwd, group, force = false, dryRun = false } = opts;
57
90
  const templatesRoot = getTemplatesRoot();
58
91
  const srcBase = path.join(templatesRoot, "commands", group);
59
92
  const outBase = path.join(cwd, ".cursor", "commands");
60
- ensureDir(outBase);
93
+ if (dryRun) {
94
+ console.log(`PLAN add ${group} (dry-run)`);
95
+ }
96
+ else {
97
+ ensureDir(outBase);
98
+ }
99
+ // Load or ensure manifest exists (for dry-run, we only read it)
100
+ let manifest = loadManifest(cwd) || createEmptyManifest(getToolVersion());
101
+ if (!dryRun && !loadManifest(cwd)) {
102
+ // For real runs, ensure manifest exists on disk
103
+ manifest = ensureManifest(cwd);
104
+ }
61
105
  const files = GROUPS[group];
62
106
  let copied = 0;
107
+ const writtenPaths = new Set();
108
+ const operations = [];
63
109
  for (const f of files) {
64
110
  const src = path.join(srcBase, f);
65
111
  const dst = path.join(outBase, f);
66
- if (!fs.existsSync(src))
112
+ // Safety: ensure path is under .cursor/commands
113
+ assertPathUnderCommands(cwd, dst);
114
+ const relPath = getRelativePath(cwd, dst);
115
+ // Check for duplicates
116
+ if (writtenPaths.has(relPath)) {
117
+ const errorMsg = `ERROR duplicate destination path: ${relPath}`;
118
+ if (dryRun) {
119
+ operations.push(errorMsg);
120
+ continue;
121
+ }
122
+ throw new Error(`Duplicate destination path: ${relPath}`);
123
+ }
124
+ writtenPaths.add(relPath);
125
+ if (!fs.existsSync(src)) {
126
+ const errorMsg = `ERROR template missing: ${src}`;
127
+ if (dryRun) {
128
+ operations.push(errorMsg);
129
+ continue;
130
+ }
67
131
  throw new Error("Template missing: " + src);
68
- if (copyIfMissing(src, dst, force))
69
- copied++;
132
+ }
133
+ // Read template content to compute hash (needed for planning)
134
+ const templateBody = fs.readFileSync(src, "utf8");
135
+ const templateContent = templateBody.includes(MANAGED_MARKER) ? templateBody : `${MANAGED_MARKER}\n\n${templateBody}`;
136
+ const templateHash = sha256(templateContent);
137
+ const exists = fs.existsSync(dst);
138
+ const hasMarker = exists ? fileContainsMarker(dst) : false;
139
+ const inManifest = manifest.files.some((mf) => mf.path === relPath);
140
+ // Tool-managed = has marker AND in manifest
141
+ const isToolManaged = exists && hasMarker && inManifest;
142
+ if (!exists) {
143
+ // File doesn't exist - will create
144
+ if (dryRun) {
145
+ operations.push(`CREATE ${relPath}`);
146
+ }
147
+ else {
148
+ writeWithMarker(src, dst);
149
+ copied++;
150
+ const templateId = `commands/${group}/${f}`;
151
+ upsertFile(manifest, {
152
+ path: relPath,
153
+ templateId,
154
+ templateVersion: 1,
155
+ contentSha256: templateHash,
156
+ });
157
+ }
158
+ }
159
+ else if (isToolManaged && force) {
160
+ // File exists, is tool-managed (marker + manifest), and --force is set - will overwrite
161
+ if (dryRun) {
162
+ operations.push(`OVERWRITE ${relPath} (tool-managed, --force)`);
163
+ }
164
+ else {
165
+ writeWithMarker(src, dst);
166
+ copied++;
167
+ const templateId = `commands/${group}/${f}`;
168
+ upsertFile(manifest, {
169
+ path: relPath,
170
+ templateId,
171
+ templateVersion: 1,
172
+ contentSha256: templateHash,
173
+ });
174
+ }
175
+ }
176
+ else if (isToolManaged && !force) {
177
+ // File exists, is tool-managed, but no --force - will skip
178
+ if (dryRun) {
179
+ operations.push(`SKIP ${relPath} (exists, tool-managed)`);
180
+ }
181
+ else {
182
+ ensureMarkerOnFile(dst);
183
+ }
184
+ }
185
+ else {
186
+ // File exists but NOT tool-managed (user-modified, orphan marker, or collision)
187
+ // Always skip, even with --force
188
+ if (dryRun) {
189
+ operations.push(`SKIP ${relPath} (exists, un-managed)`);
190
+ }
191
+ else {
192
+ ensureMarkerOnFile(dst);
193
+ }
194
+ }
70
195
  }
196
+ if (dryRun) {
197
+ for (const op of operations) {
198
+ console.log(op);
199
+ }
200
+ return;
201
+ }
202
+ // Update manifest groups (add if not present)
203
+ if (!manifest.groups.includes(group)) {
204
+ manifest.groups.push(group);
205
+ }
206
+ manifest.updatedAt = new Date().toISOString();
207
+ saveManifest(cwd, manifest);
71
208
  console.log('Installed group "' + group + '" into .cursor/commands (' + copied + " file(s) written).");
72
209
  if (group === "core")
73
210
  ensureMemory(cwd);
74
211
  }
75
212
  export function removeGroup(opts) {
76
- const { cwd, group } = opts;
213
+ const { cwd, group, dryRun = false } = opts;
214
+ // Load manifest (doctor guarantees it exists, but handle gracefully)
215
+ let manifest = loadManifest(cwd);
216
+ if (!manifest) {
217
+ if (dryRun) {
218
+ console.log(`PLAN remove ${group} (dry-run)`);
219
+ console.log("SKIP manifest missing");
220
+ return;
221
+ }
222
+ // If manifest doesn't exist, create empty one to avoid errors
223
+ manifest = ensureManifest(cwd);
224
+ }
225
+ if (dryRun) {
226
+ console.log(`PLAN remove ${group} (dry-run)`);
227
+ }
77
228
  const outBase = path.join(cwd, ".cursor", "commands");
78
- const files = GROUPS[group];
79
229
  let removed = 0;
80
230
  let skipped = 0;
81
- for (const f of files) {
82
- const dst = path.join(outBase, f);
83
- if (!fs.existsSync(dst))
231
+ const operations = [];
232
+ // Find files in manifest that belong to this group
233
+ // templateId pattern: "commands/{group}/{filename}"
234
+ const groupPrefix = `commands/${group}/`;
235
+ const manifestFiles = manifest.files.filter((f) => f.templateId.startsWith(groupPrefix));
236
+ for (const manifestFile of manifestFiles) {
237
+ const dst = path.join(cwd, manifestFile.path);
238
+ // Safety: ensure path is under .cursor/commands
239
+ assertPathUnderCommands(cwd, dst);
240
+ if (!fs.existsSync(dst)) {
241
+ // File missing on disk
242
+ if (dryRun) {
243
+ operations.push(`IGNORE ${manifestFile.path} (missing on disk)`);
244
+ }
245
+ else {
246
+ // File missing on disk, remove from manifest
247
+ removeFile(manifest, manifestFile.path);
248
+ }
84
249
  continue;
85
- // Only delete files we manage.
250
+ }
251
+ // Only delete files that have marker AND are in manifest
252
+ // Never delete just because it has a marker
86
253
  if (!fileContainsMarker(dst)) {
87
254
  skipped++;
255
+ if (dryRun) {
256
+ operations.push(`SKIP ${manifestFile.path} (not managed)`);
257
+ }
88
258
  continue;
89
259
  }
90
- fs.unlinkSync(dst);
91
- removed++;
260
+ // File has marker and is in manifest - safe to delete
261
+ if (dryRun) {
262
+ operations.push(`DELETE ${manifestFile.path}`);
263
+ }
264
+ else {
265
+ fs.unlinkSync(dst);
266
+ removeFile(manifest, manifestFile.path);
267
+ removed++;
268
+ }
269
+ }
270
+ // Check for orphan marker files (files with marker but not in manifest for this group)
271
+ if (fs.existsSync(outBase)) {
272
+ const manifestPaths = new Set(manifest.files.map((f) => f.path));
273
+ const entries = fs.readdirSync(outBase, { withFileTypes: true });
274
+ for (const entry of entries) {
275
+ if (entry.isFile()) {
276
+ const filePath = path.join(outBase, entry.name);
277
+ const relPath = path.relative(cwd, filePath).replace(/\\/g, "/");
278
+ // If file has marker but is not in manifest, it's an orphan
279
+ if (fileContainsMarker(filePath) && !manifestPaths.has(relPath)) {
280
+ if (dryRun) {
281
+ operations.push(`IGNORE ${relPath} (orphan marker, not in manifest)`);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ }
287
+ if (dryRun) {
288
+ for (const op of operations) {
289
+ console.log(op);
290
+ }
291
+ return;
292
+ }
293
+ // Remove group from manifest.groups if no files remain for this group
294
+ // Re-check after removals
295
+ const remainingGroupFiles = manifest.files.filter((f) => f.templateId.startsWith(groupPrefix));
296
+ if (remainingGroupFiles.length === 0 && manifest.groups.includes(group)) {
297
+ manifest.groups = manifest.groups.filter((g) => g !== group);
92
298
  }
299
+ manifest.updatedAt = new Date().toISOString();
300
+ saveManifest(cwd, manifest);
93
301
  console.log('Removed group "' + group + '" from .cursor/commands (' + removed + " file(s) deleted, " + skipped + " skipped).");
94
302
  }
@@ -0,0 +1,113 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { ensureMemory } from "./memory.js";
4
+ import { loadManifest, saveManifest, createEmptyManifest, MANIFEST_REL, sha256 } from "./manifest.js";
5
+ const REQUIRED = [
6
+ "docs/status.md",
7
+ "docs/todo.md",
8
+ "docs/decisions.md",
9
+ ".cursor/rules/project.md",
10
+ ];
11
+ const MANAGED_MARKER = "<!-- managed-by: 4runr-cursor-setup -->";
12
+ const COMMANDS_DIR = ".cursor/commands";
13
+ function fileContainsMarker(p) {
14
+ if (!fs.existsSync(p))
15
+ return false;
16
+ const s = fs.readFileSync(p, "utf8");
17
+ return s.includes(MANAGED_MARKER);
18
+ }
19
+ export function doctor(cwd, toolVersion, opts) {
20
+ // Run fixes first if requested
21
+ if (opts.fix) {
22
+ ensureMemory(cwd, { dryRun: !!opts.dryRun });
23
+ // Always ensure manifest exists when --fix is used
24
+ if (!loadManifest(cwd)) {
25
+ const nm = createEmptyManifest(toolVersion);
26
+ saveManifest(cwd, nm, !!opts.dryRun);
27
+ }
28
+ }
29
+ // Collect issues after fixes have run
30
+ const issues = [];
31
+ for (const rel of REQUIRED) {
32
+ if (!fs.existsSync(path.join(cwd, rel))) {
33
+ issues.push({ level: "error", code: "MISSING_FILE", detail: rel });
34
+ }
35
+ }
36
+ const m = loadManifest(cwd);
37
+ if (!m) {
38
+ issues.push({ level: "warn", code: "MISSING_MANIFEST", detail: MANIFEST_REL });
39
+ }
40
+ else {
41
+ // Check manifest entries for missing files
42
+ for (const f of m.files) {
43
+ const filePath = path.join(cwd, f.path);
44
+ if (!fs.existsSync(filePath)) {
45
+ issues.push({
46
+ level: "warn",
47
+ code: "MANIFEST_ENTRY_MISSING_ON_DISK",
48
+ detail: f.path,
49
+ });
50
+ }
51
+ else {
52
+ // Check for collisions: file exists but doesn't have marker or has wrong content
53
+ if (!fileContainsMarker(filePath)) {
54
+ issues.push({
55
+ level: "error",
56
+ code: "COLLISION_UNMANAGED_FILE",
57
+ detail: f.path,
58
+ });
59
+ }
60
+ else {
61
+ // Verify content hash matches (optional strict check)
62
+ const currentContent = fs.readFileSync(filePath, "utf8");
63
+ const currentHash = sha256(currentContent);
64
+ if (currentHash !== f.contentSha256) {
65
+ issues.push({
66
+ level: "warn",
67
+ code: "MANIFEST_FILE_MODIFIED",
68
+ detail: f.path,
69
+ });
70
+ }
71
+ }
72
+ }
73
+ }
74
+ // Check for orphan marker files in .cursor/commands (files with marker but not in manifest)
75
+ const commandsDir = path.join(cwd, COMMANDS_DIR);
76
+ if (fs.existsSync(commandsDir)) {
77
+ const manifestPaths = new Set(m.files.map((f) => f.path));
78
+ const entries = fs.readdirSync(commandsDir, { withFileTypes: true });
79
+ for (const entry of entries) {
80
+ if (entry.isFile()) {
81
+ const filePath = path.join(commandsDir, entry.name);
82
+ const relPath = path.relative(cwd, filePath).replace(/\\/g, "/");
83
+ // If file has marker but is not in manifest, it's an orphan
84
+ if (fileContainsMarker(filePath) && !manifestPaths.has(relPath)) {
85
+ issues.push({
86
+ level: "warn",
87
+ code: "ORPHAN_MARKER_FILE",
88
+ detail: relPath,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ const hasErrors = issues.some(i => i.level === "error");
96
+ if (opts.json) {
97
+ console.log(JSON.stringify({ ok: !hasErrors, issues }, null, 2));
98
+ }
99
+ else {
100
+ if (issues.length === 0)
101
+ console.log("✅ doctor: OK");
102
+ else {
103
+ console.log("doctor report:");
104
+ for (const i of issues) {
105
+ console.log(`- ${i.level.toUpperCase()} ${i.code}: ${i.detail}`);
106
+ }
107
+ }
108
+ }
109
+ if (opts.strict && issues.length > 0)
110
+ process.exitCode = 1;
111
+ if (hasErrors)
112
+ process.exitCode = 1;
113
+ }
@@ -0,0 +1,43 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ export const MANIFEST_REL = ".cursor/4runr.manifest.json";
5
+ export function sha256(s) {
6
+ return crypto.createHash("sha256").update(s, "utf8").digest("hex");
7
+ }
8
+ export function loadManifest(cwd) {
9
+ const fp = path.join(cwd, MANIFEST_REL);
10
+ if (!fs.existsSync(fp))
11
+ return null;
12
+ return JSON.parse(fs.readFileSync(fp, "utf8"));
13
+ }
14
+ export function saveManifest(cwd, m, dryRun = false) {
15
+ if (dryRun)
16
+ return;
17
+ const fp = path.join(cwd, MANIFEST_REL);
18
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
19
+ fs.writeFileSync(fp, JSON.stringify(m, null, 2) + "\n", "utf8");
20
+ }
21
+ export function createEmptyManifest(toolVersion) {
22
+ return {
23
+ schemaVersion: 1,
24
+ tool: "4runr-cursor-setup",
25
+ toolVersion,
26
+ updatedAt: new Date().toISOString(),
27
+ groups: [],
28
+ files: [],
29
+ };
30
+ }
31
+ export function upsertFile(m, rec) {
32
+ const i = m.files.findIndex(f => f.path === rec.path);
33
+ if (i >= 0)
34
+ m.files[i] = rec;
35
+ else
36
+ m.files.push(rec);
37
+ }
38
+ export function removeFile(m, relPath) {
39
+ m.files = m.files.filter(f => f.path !== relPath);
40
+ }
41
+ export function hasFile(m, relPath) {
42
+ return m.files.some(f => f.path === relPath);
43
+ }
@@ -1,35 +1,19 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import * as url from "url";
4
- function ensureDir(p) {
5
- fs.mkdirSync(p, { recursive: true });
6
- }
7
- function copyIfMissing(src, dst) {
8
- if (fs.existsSync(dst))
9
- return false;
10
- ensureDir(path.dirname(dst));
11
- fs.copyFileSync(src, dst);
12
- return true;
13
- }
14
- function getTemplatesRoot() {
15
- const here = path.dirname(url.fileURLToPath(import.meta.url));
16
- return path.resolve(here, "..", "templates");
17
- }
18
- export function ensureMemory(cwd) {
19
- const root = getTemplatesRoot();
20
- const srcBase = path.join(root, "memory");
21
- const mappings = [
22
- { src: path.join(srcBase, "docs", "status.md"), dst: path.join(cwd, "docs", "status.md") },
23
- { src: path.join(srcBase, "docs", "todo.md"), dst: path.join(cwd, "docs", "todo.md") },
24
- { src: path.join(srcBase, "docs", "decisions.md"), dst: path.join(cwd, "docs", "decisions.md") },
25
- { src: path.join(srcBase, "cursor", "project.md"), dst: path.join(cwd, ".cursor", "rules", "project.md") },
1
+ import fs from "fs";
2
+ import path from "path";
3
+ export function ensureMemory(cwd, opts) {
4
+ const files = [
5
+ "docs/status.md",
6
+ "docs/todo.md",
7
+ "docs/decisions.md",
8
+ ".cursor/rules/project.md",
26
9
  ];
27
- let created = 0;
28
- for (const m of mappings) {
29
- if (!fs.existsSync(m.src))
30
- throw new Error("Template missing: " + m.src);
31
- if (copyIfMissing(m.src, m.dst))
32
- created++;
10
+ for (const rel of files) {
11
+ const fp = path.join(cwd, rel);
12
+ if (fs.existsSync(fp))
13
+ continue;
14
+ if (!opts?.dryRun) {
15
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
16
+ fs.writeFileSync(fp, "", "utf8");
17
+ }
33
18
  }
34
- console.log(`Memory ensured (created ${created} file(s), left existing untouched).`);
35
19
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "4runr-cursor-setup",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
7
+ "test": "node --test test/*.test.js",
8
8
  "dev": "tsx src/cli.ts",
9
9
  "build": "tsc",
10
10
  "prepublishOnly": "npm run build",