claude-setup 1.1.1 → 1.1.2

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.
@@ -1,23 +1,135 @@
1
1
  import { readManifest } from "../manifest.js";
2
2
  import { readState } from "../state.js";
3
+ import { detectOS } from "../os.js";
4
+ import { c, statusLine, section } from "../output.js";
5
+ function safeJsonParse(content) {
6
+ try {
7
+ return JSON.parse(content);
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
3
13
  export async function runStatus() {
4
14
  const manifest = await readManifest();
5
15
  const state = await readState();
16
+ const os = detectOS();
6
17
  if (!manifest) {
7
- console.log("No setup found.\n Run: npx claude-setup init");
18
+ console.log(`${c.yellow("⚠️ No setup found.")}\n Run: ${c.cyan("npx claude-setup init")}`);
8
19
  return;
9
20
  }
10
21
  const last = manifest.runs.at(-1);
11
- console.log(`Last: ${last.command} at ${last.at} (v${last.claudeStackVersion})\n`);
12
- console.log(`CLAUDE.md ${state.claudeMd.exists ? "✅" : "❌ missing"}`);
13
- console.log(`.mcp.json ${state.mcpJson.exists ? "" : "❌ missing"}`);
14
- console.log(`settings.json ${state.settings.exists ? "✅" : "❌ missing"}`);
15
- console.log(`Skills ${state.skills.length || "none"}`);
16
- console.log(`Workflows ${state.workflows.length || "none"}`);
17
- console.log(`\nHistory (last 5):`);
22
+ const version = last.claudeStackVersion ?? "unknown";
23
+ // --- Header ---
24
+ console.log(c.bold("status") + ` — ${new Date().toISOString().split("T")[0]}\n`);
25
+ // --- Project info ---
26
+ const projectType = inferProjectType(state);
27
+ console.log(`Project : ${projectType}`);
28
+ console.log(`OS : ${os}`);
29
+ console.log(`Version : claude-setup v${version}`);
30
+ // --- Setup files ---
31
+ section("Setup files");
32
+ // CLAUDE.md
33
+ if (state.claudeMd.exists) {
34
+ statusLine("✅", "CLAUDE.md", "exists");
35
+ }
36
+ else {
37
+ statusLine("❌", "CLAUDE.md", "missing");
38
+ }
39
+ // .mcp.json
40
+ if (state.mcpJson.exists && state.mcpJson.content) {
41
+ const mcp = safeJsonParse(state.mcpJson.content);
42
+ const serverCount = mcp?.mcpServers
43
+ ? Object.keys(mcp.mcpServers).length
44
+ : 0;
45
+ statusLine("✅", ".mcp.json", `${serverCount} server(s)`);
46
+ }
47
+ else {
48
+ statusLine("❌", ".mcp.json", "missing");
49
+ }
50
+ // settings.json
51
+ if (state.settings.exists && state.settings.content) {
52
+ const settings = safeJsonParse(state.settings.content);
53
+ let hookCount = 0;
54
+ if (settings) {
55
+ for (const key of ["PreToolUse", "PostToolUse", "PreCompact", "PostCompact", "Notification", "Stop", "SubagentStop"]) {
56
+ const hooks = settings[key];
57
+ if (Array.isArray(hooks))
58
+ hookCount += hooks.length;
59
+ }
60
+ }
61
+ statusLine("✅", "settings.json", `${hookCount} hook(s)`);
62
+ }
63
+ else {
64
+ statusLine("❌", "settings.json", "missing");
65
+ }
66
+ // Lists
67
+ console.log(` Skills : ${state.skills.length ? state.skills.map(s => s.split("/").at(-2) ?? s).join(", ") : "none"}`);
68
+ console.log(` Commands : ${state.commands.length ? state.commands.map(s => s.split("/").pop()?.replace(".md", "") ?? s).join(", ") : "none"}`);
69
+ console.log(` Workflows : ${state.workflows.length ? state.workflows.map(s => s.split("/").pop() ?? s).join(", ") : "none"}`);
70
+ // --- Run history ---
71
+ section("Run history (last 5)");
18
72
  for (const r of manifest.runs.slice(-5)) {
19
- console.log(` ${r.at} ${r.command}${r.input ? ` — "${r.input}"` : ""}`);
73
+ const inputStr = r.input ? ` — "${r.input}"` : "";
74
+ console.log(` ${c.dim(r.at)} ${r.command}${inputStr}`);
75
+ }
76
+ // --- Health hint ---
77
+ const hint = getHealthHint(manifest, state);
78
+ if (hint) {
79
+ section("Health hint");
80
+ console.log(` ${hint}`);
20
81
  }
21
- console.log("\n npx claude-setup sync — update after changes");
22
- console.log(" npx claude-setup doctor — validate environment");
82
+ // --- Next action ---
83
+ const next = getNextAction(manifest, state);
84
+ if (next) {
85
+ section("Next action");
86
+ console.log(` ${next}`);
87
+ }
88
+ console.log("");
89
+ }
90
+ function inferProjectType(state) {
91
+ if (state.claudeMd.content) {
92
+ // Try to infer from CLAUDE.md content
93
+ const content = state.claudeMd.content.toLowerCase();
94
+ if (content.includes("typescript") || content.includes("node"))
95
+ return "Node.js / TypeScript";
96
+ if (content.includes("python"))
97
+ return "Python";
98
+ if (content.includes("go ") || content.includes("golang"))
99
+ return "Go";
100
+ if (content.includes("rust"))
101
+ return "Rust";
102
+ if (content.includes("ruby"))
103
+ return "Ruby";
104
+ if (content.includes("java") && !content.includes("javascript"))
105
+ return "Java";
106
+ }
107
+ return c.dim("unknown — run init to detect");
108
+ }
109
+ function getHealthHint(manifest, _state) {
110
+ const last = manifest.runs.at(-1);
111
+ if (!last)
112
+ return `${c.yellow("⚠️")} No runs recorded. Run: ${c.cyan("npx claude-setup init")}`;
113
+ const daysSince = Math.floor((Date.now() - new Date(last.at).getTime()) / (1000 * 60 * 60 * 24));
114
+ // Check for recent deletions
115
+ if (last.command === "sync") {
116
+ const snapshot = last.snapshot;
117
+ const deletionCount = Object.keys(snapshot).filter(k => k.startsWith("[deleted]")).length;
118
+ if (deletionCount > 0) {
119
+ return `${c.red("🔴")} Last sync detected ${deletionCount} deletion(s). Verify setup is still valid.`;
120
+ }
121
+ }
122
+ if (daysSince > 7) {
123
+ return `${c.yellow("⚠️")} ${daysSince} day(s) since last sync. Source files may have drifted.`;
124
+ }
125
+ return `${c.green("✅")} Setup looks current.`;
126
+ }
127
+ function getNextAction(manifest, _state) {
128
+ const last = manifest.runs.at(-1);
129
+ if (!last)
130
+ return `Run ${c.cyan("npx claude-setup init")} to set up your project.`;
131
+ const daysSince = Math.floor((Date.now() - new Date(last.at).getTime()) / (1000 * 60 * 60 * 24));
132
+ if (daysSince > 7)
133
+ return `Run ${c.cyan("npx claude-setup sync")} to check for changes.`;
134
+ return null;
23
135
  }
@@ -1 +1,3 @@
1
- export declare function runSync(): Promise<void>;
1
+ export declare function runSync(opts?: {
2
+ dryRun?: boolean;
3
+ }): Promise<void>;
@@ -3,6 +3,7 @@ import { collectProjectFiles } from "../collect.js";
3
3
  import { readState } from "../state.js";
4
4
  import { readManifest, sha256, updateManifest } from "../manifest.js";
5
5
  import { buildSyncCommand } from "../builder.js";
6
+ import { c } from "../output.js";
6
7
  function ensureDir(dir) {
7
8
  if (!existsSync(dir))
8
9
  mkdirSync(dir, { recursive: true });
@@ -21,6 +22,9 @@ function computeDiff(snapshot, collected) {
21
22
  const changed = [];
22
23
  const deleted = [];
23
24
  for (const [path, content] of Object.entries(current)) {
25
+ // Skip virtual keys — they're not real files
26
+ if (path === "__digest__")
27
+ continue;
24
28
  const hash = sha256(content);
25
29
  if (!snapshot[path]) {
26
30
  added.push({ path, content: truncate(content, 2000) });
@@ -30,34 +34,58 @@ function computeDiff(snapshot, collected) {
30
34
  }
31
35
  }
32
36
  for (const path of Object.keys(snapshot)) {
37
+ // Skip virtual keys
38
+ if (path === "__digest__")
39
+ continue;
33
40
  if (!current[path])
34
41
  deleted.push(path);
35
42
  }
36
43
  return { added, changed, deleted };
37
44
  }
38
- export async function runSync() {
45
+ export async function runSync(opts = {}) {
46
+ const dryRun = opts.dryRun ?? false;
39
47
  const manifest = await readManifest();
40
48
  if (!manifest?.runs.length) {
41
- console.log("No previous run found. Start with: npx claude-setup init");
49
+ console.log(`No previous run found. Start with: ${c.cyan("npx claude-setup init")}`);
42
50
  return;
43
51
  }
44
52
  const lastRun = manifest.runs.at(-1);
45
53
  const collected = await collectProjectFiles(process.cwd(), "normal");
46
54
  const diff = computeDiff(lastRun.snapshot, collected);
47
55
  if (!diff.added.length && !diff.changed.length && !diff.deleted.length) {
48
- console.log(`✅ No changes since ${lastRun.at}. Setup is current.`);
56
+ console.log(`${c.green("✅")} No changes since ${c.dim(lastRun.at)}. Setup is current.`);
49
57
  return;
50
58
  }
51
59
  const state = await readState();
52
60
  const content = buildSyncCommand(diff, collected, state);
61
+ if (dryRun) {
62
+ console.log(c.bold("[DRY RUN] Changes detected:\n"));
63
+ if (diff.added.length) {
64
+ console.log(c.green(` +${diff.added.length} added`));
65
+ for (const f of diff.added)
66
+ console.log(` ${f.path}`);
67
+ }
68
+ if (diff.changed.length) {
69
+ console.log(c.yellow(` ~${diff.changed.length} modified`));
70
+ for (const f of diff.changed)
71
+ console.log(` ${f.path}`);
72
+ }
73
+ if (diff.deleted.length) {
74
+ console.log(c.red(` -${diff.deleted.length} deleted`));
75
+ for (const f of diff.deleted)
76
+ console.log(` ${f}`);
77
+ }
78
+ console.log(`\n Would write: .claude/commands/stack-sync.md (~${Math.ceil(content.length / 4)} tokens)`);
79
+ return;
80
+ }
53
81
  ensureDir(".claude/commands");
54
82
  writeFileSync(".claude/commands/stack-sync.md", content, "utf8");
55
83
  await updateManifest("sync", collected);
56
84
  console.log(`
57
- Changes since ${lastRun.at}:
58
- +${diff.added.length} added ~${diff.changed.length} modified -${diff.deleted.length} deleted
85
+ Changes since ${c.dim(lastRun.at)}:
86
+ ${c.green(`+${diff.added.length}`)} added ${c.yellow(`~${diff.changed.length}`)} modified ${c.red(`-${diff.deleted.length}`)} deleted
59
87
 
60
- ✅ Ready. Open Claude Code and run:
61
- /stack-sync
88
+ ${c.green("")} Ready. Open Claude Code and run:
89
+ ${c.cyan("/stack-sync")}
62
90
  `);
63
91
  }
package/dist/config.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ export interface TruncationRule {
2
+ maxLines?: number;
3
+ metadataOnly?: boolean;
4
+ maxBytes?: number;
5
+ }
1
6
  export interface SetupConfig {
2
7
  maxSourceFiles: number;
3
8
  maxDepth: number;
@@ -11,5 +16,14 @@ export interface SetupConfig {
11
16
  digestMode: boolean;
12
17
  extraBlockedDirs: string[];
13
18
  sourceDirs: string[];
19
+ truncationRules: Record<string, TruncationRule>;
14
20
  }
15
21
  export declare function loadConfig(cwd?: string): SetupConfig;
22
+ /**
23
+ * Auto-generate .claude-setup.json with sensible defaults.
24
+ * Only creates if it doesn't exist — never overwrites.
25
+ * Returns true if created, false if already existed.
26
+ */
27
+ export declare function ensureConfig(cwd?: string): boolean;
28
+ /** Apply truncation rule to file content */
29
+ export declare function applyTruncation(filename: string, content: string, config: SetupConfig): string;
package/dist/config.js CHANGED
@@ -1,5 +1,18 @@
1
- import { readFileSync, existsSync } from "fs";
1
+ import { readFileSync, writeFileSync, existsSync } from "fs";
2
2
  import { join } from "path";
3
+ // --- Defaults ---
4
+ // Sensible for projects up to ~200 source files.
5
+ // For bigger projects, the developer can increase budgets in .claude-setup.json.
6
+ const DEFAULT_TRUNCATION_RULES = {
7
+ "package-lock.json": { metadataOnly: true },
8
+ "Dockerfile": { maxLines: 50 },
9
+ "docker-compose.yml": { maxLines: 100, maxBytes: 8000 },
10
+ "docker-compose.yaml": { maxLines: 100, maxBytes: 8000 },
11
+ "pom.xml": { maxLines: 80 },
12
+ "build.gradle": { maxLines: 80 },
13
+ "build.gradle.kts": { maxLines: 80 },
14
+ "setup.py": { maxLines: 60 },
15
+ };
3
16
  const DEFAULTS = {
4
17
  maxSourceFiles: 15,
5
18
  maxDepth: 6,
@@ -13,14 +26,21 @@ const DEFAULTS = {
13
26
  digestMode: true,
14
27
  extraBlockedDirs: [],
15
28
  sourceDirs: [],
29
+ truncationRules: DEFAULT_TRUNCATION_RULES,
16
30
  };
17
31
  const CONFIG_FILENAME = ".claude-setup.json";
18
32
  export function loadConfig(cwd = process.cwd()) {
19
33
  const configPath = join(cwd, CONFIG_FILENAME);
20
34
  if (!existsSync(configPath))
21
- return { ...DEFAULTS };
35
+ return { ...DEFAULTS, truncationRules: { ...DEFAULT_TRUNCATION_RULES } };
22
36
  try {
23
37
  const raw = JSON.parse(readFileSync(configPath, "utf8"));
38
+ // Merge truncation rules: user overrides win, defaults fill gaps
39
+ const userRules = raw.truncationRules ?? {};
40
+ const mergedRules = { ...DEFAULT_TRUNCATION_RULES };
41
+ for (const [file, rule] of Object.entries(userRules)) {
42
+ mergedRules[file] = rule;
43
+ }
24
44
  return {
25
45
  maxSourceFiles: raw.maxSourceFiles ?? DEFAULTS.maxSourceFiles,
26
46
  maxDepth: raw.maxDepth ?? DEFAULTS.maxDepth,
@@ -34,9 +54,75 @@ export function loadConfig(cwd = process.cwd()) {
34
54
  digestMode: raw.digestMode ?? DEFAULTS.digestMode,
35
55
  extraBlockedDirs: raw.extraBlockedDirs ?? DEFAULTS.extraBlockedDirs,
36
56
  sourceDirs: raw.sourceDirs ?? DEFAULTS.sourceDirs,
57
+ truncationRules: mergedRules,
37
58
  };
38
59
  }
39
60
  catch {
40
- return { ...DEFAULTS };
61
+ return { ...DEFAULTS, truncationRules: { ...DEFAULT_TRUNCATION_RULES } };
62
+ }
63
+ }
64
+ /**
65
+ * Auto-generate .claude-setup.json with sensible defaults.
66
+ * Only creates if it doesn't exist — never overwrites.
67
+ * Returns true if created, false if already existed.
68
+ */
69
+ export function ensureConfig(cwd = process.cwd()) {
70
+ const configPath = join(cwd, CONFIG_FILENAME);
71
+ if (existsSync(configPath))
72
+ return false;
73
+ const config = {
74
+ maxSourceFiles: DEFAULTS.maxSourceFiles,
75
+ maxDepth: DEFAULTS.maxDepth,
76
+ maxFileSizeKB: DEFAULTS.maxFileSizeKB,
77
+ tokenBudget: DEFAULTS.tokenBudget,
78
+ digestMode: DEFAULTS.digestMode,
79
+ extraBlockedDirs: DEFAULTS.extraBlockedDirs,
80
+ sourceDirs: DEFAULTS.sourceDirs,
81
+ truncationRules: DEFAULT_TRUNCATION_RULES,
82
+ };
83
+ try {
84
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
85
+ return true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ /** Apply truncation rule to file content */
92
+ export function applyTruncation(filename, content, config) {
93
+ const rule = config.truncationRules[filename];
94
+ if (!rule) {
95
+ // No rule — use generic cap
96
+ return content.length > 4000 ? content.slice(0, 4000) + "\n[... truncated]" : content;
97
+ }
98
+ // Metadata-only extraction (e.g. package-lock.json)
99
+ if (rule.metadataOnly) {
100
+ try {
101
+ const parsed = JSON.parse(content);
102
+ return JSON.stringify({
103
+ name: parsed.name,
104
+ version: parsed.version,
105
+ lockfileVersion: parsed.lockfileVersion,
106
+ }, null, 2);
107
+ }
108
+ catch {
109
+ return `[${filename}: could not parse]`;
110
+ }
111
+ }
112
+ // maxBytes check first: if file is small enough, content passes through before line truncation
113
+ if (rule.maxBytes && content.length <= rule.maxBytes) {
114
+ return content;
115
+ }
116
+ // Line-based truncation
117
+ if (rule.maxLines) {
118
+ const lines = content.split("\n");
119
+ if (lines.length <= rule.maxLines)
120
+ return content;
121
+ return lines.slice(0, rule.maxLines).join("\n") + `\n[... ${lines.length - rule.maxLines} more lines truncated]`;
122
+ }
123
+ // Byte cap
124
+ if (rule.maxBytes && content.length > rule.maxBytes) {
125
+ return content.slice(0, rule.maxBytes) + "\n[... truncated]";
41
126
  }
127
+ return content;
42
128
  }
package/dist/doctor.d.ts CHANGED
@@ -1 +1 @@
1
- export declare function runDoctor(): Promise<void>;
1
+ export declare function runDoctor(verbose?: boolean): Promise<void>;