4runr-cursor-setup 0.1.11 → 0.1.15

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 ADDED
@@ -0,0 +1,77 @@
1
+ # 4runr-cursor-setup
2
+
3
+ ## Golden Path
4
+
5
+ ```bash
6
+ # Repair / normalize any repo (safe)
7
+ npx -y 4runr-cursor-setup@latest doctor --fix --purge-orphans --strict
8
+
9
+ # Install core commands
10
+ npx -y 4runr-cursor-setup@latest add core
11
+
12
+ # CI / health check (fails if issues)
13
+ npx -y 4runr-cursor-setup@latest doctor --strict
14
+
15
+ # Machine-readable audit (for automation)
16
+ npx -y 4runr-cursor-setup@latest doctor --json
17
+ ```
18
+
19
+ ## Contract / Invariants
20
+
21
+ - Tool never overwrites un-managed files (even with `--force`)
22
+ - `--force` overwrites only tool-managed files (marker + manifest)
23
+ - `remove <group>` deletes only manifest-owned files (marker + manifest entry)
24
+ - `doctor --purge-orphans` deletes only marker files not in manifest
25
+ - `doctor --strict` exits non-zero only when final issues exist
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ # List available command groups
31
+ npx -y 4runr-cursor-setup@latest list
32
+
33
+ # Add a command group
34
+ npx -y 4runr-cursor-setup@latest add <group> [--force] [--dry-run]
35
+
36
+ # Remove a command group
37
+ npx -y 4runr-cursor-setup@latest remove <group> [--dry-run]
38
+
39
+ # Audit and repair
40
+ npx -y 4runr-cursor-setup@latest doctor [--fix] [--strict] [--json] [--dry-run] [--purge-orphans]
41
+ ```
42
+
43
+ ## Available Groups
44
+
45
+ - `core`: Essential 4runr commands (start, close)
46
+ - `planning`: Planning commands (phase, task)
47
+ - `governance`: Governance commands (decision, scope-change)
48
+ - `debugging`: Debugging commands (repro, verify)
49
+
50
+ ## Troubleshooting
51
+
52
+ ### npx Cache Issues
53
+
54
+ If `npx -y 4runr-cursor-setup@latest` fails with `ETARGET` but the version exists on npm:
55
+
56
+ This is an npx cache desync. Clear the cache and retry:
57
+
58
+ ```bash
59
+ npm cache clean --force
60
+ Remove-Item -Recurse -Force "$env:LOCALAPPDATA\npm-cache\_npx" -ErrorAction SilentlyContinue
61
+ ```
62
+
63
+ ## Development
64
+
65
+ ```bash
66
+ # Build
67
+ npm run build
68
+
69
+ # Run tests
70
+ npm test
71
+
72
+ # Run smoke test
73
+ npm run test:smoke
74
+
75
+ # Development mode
76
+ npm run dev
77
+ ```
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ function usage() {
9
9
  console.log(" list");
10
10
  console.log(" add <group> [--force] [--dry-run]");
11
11
  console.log(" remove <group> [--dry-run]");
12
- console.log(" doctor [--fix] [--strict] [--json] [--dry-run] [--purge-orphans]");
12
+ console.log(" doctor [--fix] [--strict] [--json] [--dry-run] [--purge-orphans] [--plan]");
13
13
  console.log("");
14
14
  console.log("Groups: " + Object.keys(GROUPS).join(", "));
15
15
  console.log("");
@@ -69,6 +69,7 @@ if (cmd === "doctor") {
69
69
  strict: has("--strict"),
70
70
  dryRun: has("--dry-run"),
71
71
  purgeOrphans: has("--purge-orphans"),
72
+ plan: has("--plan"),
72
73
  });
73
74
  // Exit with the exit code set by doctor (or 0 if not set)
74
75
  process.exit(process.exitCode || 0);
@@ -185,24 +185,52 @@ export function doctor(cwd, toolVersion, opts) {
185
185
  }
186
186
  }
187
187
  const hasErrors = issues.some(i => i.level === "error");
188
+ // Generate plan if requested
189
+ let planItems = [];
190
+ if (opts.plan) {
191
+ planItems = generatePlan(issues, m || null);
192
+ }
188
193
  if (opts.json) {
189
194
  const jsonOutput = { ok: !hasErrors, issues };
190
195
  if (purgedInfo) {
191
196
  jsonOutput.purged = { count: purgedInfo.count, files: purgedInfo.files };
192
197
  }
198
+ if (opts.plan) {
199
+ jsonOutput.plan = planItems;
200
+ }
193
201
  console.log(JSON.stringify(jsonOutput, null, 2));
194
202
  }
195
203
  else {
196
- if (issues.length === 0)
197
- console.log("✅ doctor: OK");
204
+ if (opts.plan) {
205
+ // Plan output mode
206
+ if (planItems.length === 0) {
207
+ console.log("PLAN (doctor)");
208
+ console.log("No actions needed.");
209
+ }
210
+ else {
211
+ console.log("PLAN (doctor)");
212
+ for (let i = 0; i < planItems.length; i++) {
213
+ const item = planItems[i];
214
+ console.log(`${i + 1}) ${item.code}: ${item.detail}`);
215
+ for (const step of item.steps) {
216
+ console.log(` - ${step}`);
217
+ }
218
+ }
219
+ }
220
+ }
198
221
  else {
199
- console.log("doctor report:");
200
- for (const i of issues) {
201
- let line = `- ${i.level.toUpperCase()} ${i.code}: ${i.detail}`;
202
- if (i.hint) {
203
- line += `\n ${i.hint}`;
222
+ // Normal output mode
223
+ if (issues.length === 0)
224
+ console.log("✅ doctor: OK");
225
+ else {
226
+ console.log("doctor report:");
227
+ for (const i of issues) {
228
+ let line = `- ${i.level.toUpperCase()} ${i.code}: ${i.detail}`;
229
+ if (i.hint) {
230
+ line += `\n ${i.hint}`;
231
+ }
232
+ console.log(line);
204
233
  }
205
- console.log(line);
206
234
  }
207
235
  }
208
236
  }
@@ -212,3 +240,103 @@ export function doctor(cwd, toolVersion, opts) {
212
240
  if (hasErrors)
213
241
  process.exitCode = 1;
214
242
  }
243
+ function inferGroupFromPath(filePath, manifest) {
244
+ // Try to find the file in manifest to get templateId
245
+ if (manifest) {
246
+ const fileEntry = manifest.files.find((f) => f.path === filePath);
247
+ if (fileEntry && fileEntry.templateId) {
248
+ // templateId format: "commands/{group}/{filename}"
249
+ const parts = fileEntry.templateId.split("/");
250
+ if (parts.length >= 2 && parts[0] === "commands") {
251
+ return parts[1];
252
+ }
253
+ }
254
+ }
255
+ // Try to infer from filename pattern (e.g., "4runr-start.md" -> "core")
256
+ const filename = path.basename(filePath);
257
+ if (filename.startsWith("4runr-")) {
258
+ // Map common patterns (this is a fallback, not perfect)
259
+ if (filename.includes("start") || filename.includes("close"))
260
+ return "core";
261
+ if (filename.includes("task") || filename.includes("phase"))
262
+ return "planning";
263
+ if (filename.includes("decision") || filename.includes("scope"))
264
+ return "governance";
265
+ if (filename.includes("repro") || filename.includes("verify"))
266
+ return "debugging";
267
+ }
268
+ return null;
269
+ }
270
+ function generatePlan(issues, manifest) {
271
+ const planItems = [];
272
+ for (const issue of issues) {
273
+ let planItem = null;
274
+ switch (issue.code) {
275
+ case "MISSING_MANIFEST":
276
+ planItem = {
277
+ code: issue.code,
278
+ detail: issue.detail,
279
+ action: "Create baseline manifest",
280
+ steps: [
281
+ "npx -y 4runr-cursor-setup@latest doctor --fix"
282
+ ]
283
+ };
284
+ break;
285
+ case "ORPHAN_MARKER_FILE":
286
+ planItem = {
287
+ code: issue.code,
288
+ detail: issue.detail,
289
+ action: "Purge orphan marker files",
290
+ steps: [
291
+ "npx -y 4runr-cursor-setup@latest doctor --purge-orphans",
292
+ "npx -y 4runr-cursor-setup@latest doctor --strict"
293
+ ]
294
+ };
295
+ break;
296
+ case "COLLISION_UNMANAGED_FILE":
297
+ const group = inferGroupFromPath(issue.detail, manifest);
298
+ const groupPlaceholder = group || "<group>";
299
+ planItem = {
300
+ code: issue.code,
301
+ detail: issue.detail,
302
+ action: "Resolve collision at managed path",
303
+ steps: [
304
+ `Manual: rename or move ${issue.detail}`,
305
+ `Then run: npx -y 4runr-cursor-setup@latest add ${groupPlaceholder}`
306
+ ]
307
+ };
308
+ break;
309
+ case "MANIFEST_ENTRY_MISSING_ON_DISK":
310
+ const missingGroup = inferGroupFromPath(issue.detail, manifest);
311
+ const missingGroupPlaceholder = missingGroup || "<group>";
312
+ planItem = {
313
+ code: issue.code,
314
+ detail: issue.detail,
315
+ action: "Restore missing tool-managed file",
316
+ steps: [
317
+ `Run: npx -y 4runr-cursor-setup@latest add ${missingGroupPlaceholder}`,
318
+ `If still missing: npx -y 4runr-cursor-setup@latest add ${missingGroupPlaceholder} --force`
319
+ ]
320
+ };
321
+ break;
322
+ case "MANIFEST_FILE_MODIFIED":
323
+ const modifiedGroup = inferGroupFromPath(issue.detail, manifest);
324
+ const modifiedGroupPlaceholder = modifiedGroup || "<group>";
325
+ planItem = {
326
+ code: issue.code,
327
+ detail: issue.detail,
328
+ action: "Resolve modified managed file",
329
+ steps: [
330
+ `Manual: keep file as-is (tool will not overwrite), OR`,
331
+ `Manual: delete ${issue.detail} if you want template restored`,
332
+ `Then run: npx -y 4runr-cursor-setup@latest add ${modifiedGroupPlaceholder}`
333
+ ]
334
+ };
335
+ break;
336
+ }
337
+ if (planItem) {
338
+ planItems.push(planItem);
339
+ }
340
+ }
341
+ return planItems;
342
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "4runr-cursor-setup",
3
- "version": "0.1.11",
3
+ "version": "0.1.15",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "node --test test/*.test.js",
8
+ "test:smoke": "powershell -ExecutionPolicy Bypass -File scripts/smoke.ps1",
8
9
  "dev": "tsx src/cli.ts",
9
10
  "build": "tsc",
10
11
  "prepublishOnly": "npm run build",
@@ -0,0 +1,87 @@
1
+ # Smoke test for 4runr-cursor-setup golden path
2
+ # Runs in a temp directory to avoid dirtying the repo
3
+
4
+ $ErrorActionPreference = "Stop"
5
+
6
+ # Get the repo root (where package.json lives)
7
+ $repoRoot = Split-Path -Parent $PSScriptRoot
8
+ $cliPath = Join-Path $repoRoot "dist\cli.js"
9
+
10
+ if (-not (Test-Path $cliPath)) {
11
+ Write-Host "ERROR: dist/cli.js not found. Run 'npm run build' first." -ForegroundColor Red
12
+ exit 1
13
+ }
14
+
15
+ # Create temp directory for testing
16
+ $tempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "4runr-smoke-$(New-Guid)")
17
+ New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
18
+
19
+ try {
20
+ Write-Host "=== Smoke Test: Golden Path ===" -ForegroundColor Cyan
21
+ Write-Host "Test directory: $tempDir" -ForegroundColor Gray
22
+ Write-Host ""
23
+
24
+ # Step 1: doctor --fix --purge-orphans --strict
25
+ Write-Host "Step 1: doctor --fix --purge-orphans --strict" -ForegroundColor Yellow
26
+ Push-Location $tempDir
27
+ $exitCode = 0
28
+ node $cliPath doctor --fix --purge-orphans --strict 2>&1 | Out-String | Write-Host
29
+ if ($LASTEXITCODE -ne 0) {
30
+ Write-Host "FAILED: doctor --fix --purge-orphans --strict exited with code $LASTEXITCODE" -ForegroundColor Red
31
+ $exitCode = 1
32
+ } else {
33
+ Write-Host "PASSED: doctor --fix --purge-orphans --strict" -ForegroundColor Green
34
+ }
35
+ Pop-Location
36
+ Write-Host ""
37
+
38
+ if ($exitCode -ne 0) {
39
+ exit $exitCode
40
+ }
41
+
42
+ # Step 2: add core
43
+ Write-Host "Step 2: add core" -ForegroundColor Yellow
44
+ Push-Location $tempDir
45
+ node $cliPath add core 2>&1 | Out-String | Write-Host
46
+ if ($LASTEXITCODE -ne 0) {
47
+ Write-Host "FAILED: add core exited with code $LASTEXITCODE" -ForegroundColor Red
48
+ $exitCode = 1
49
+ } else {
50
+ Write-Host "PASSED: add core" -ForegroundColor Green
51
+ }
52
+ Pop-Location
53
+ Write-Host ""
54
+
55
+ if ($exitCode -ne 0) {
56
+ exit $exitCode
57
+ }
58
+
59
+ # Step 3: doctor --strict (should pass after fix + add)
60
+ Write-Host "Step 3: doctor --strict" -ForegroundColor Yellow
61
+ Push-Location $tempDir
62
+ node $cliPath doctor --strict 2>&1 | Out-String | Write-Host
63
+ if ($LASTEXITCODE -ne 0) {
64
+ Write-Host "FAILED: doctor --strict exited with code $LASTEXITCODE" -ForegroundColor Red
65
+ $exitCode = 1
66
+ } else {
67
+ Write-Host "PASSED: doctor --strict" -ForegroundColor Green
68
+ }
69
+ Pop-Location
70
+ Write-Host ""
71
+
72
+ if ($exitCode -ne 0) {
73
+ exit $exitCode
74
+ }
75
+
76
+ Write-Host "=== All smoke tests passed ===" -ForegroundColor Green
77
+ exit 0
78
+
79
+ } catch {
80
+ Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
81
+ exit 1
82
+ } finally {
83
+ # Cleanup temp directory
84
+ if (Test-Path $tempDir) {
85
+ Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
86
+ }
87
+ }