@aslomon/effectum 0.1.6 → 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/bin/lib/ui.js ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Shared @clack/prompts helpers and display utilities.
3
+ * Uses dynamic import() because @clack/prompts is ESM-only.
4
+ */
5
+ "use strict";
6
+
7
+ const {
8
+ STACK_CHOICES,
9
+ LANGUAGE_CHOICES,
10
+ AUTONOMY_CHOICES,
11
+ MCP_SERVERS,
12
+ } = require("./constants");
13
+
14
+ /** @type {import("@clack/prompts")} */
15
+ let p;
16
+
17
+ /**
18
+ * Initialize @clack/prompts (ESM module). Must be called once before using prompts.
19
+ * @returns {Promise<import("@clack/prompts")>}
20
+ */
21
+ async function initClack() {
22
+ if (!p) {
23
+ p = await import("@clack/prompts");
24
+ }
25
+ return p;
26
+ }
27
+
28
+ /**
29
+ * Get the loaded clack instance (for use in install.js / reconfigure.js).
30
+ * @returns {import("@clack/prompts")}
31
+ */
32
+ function getClack() {
33
+ if (!p) throw new Error("Call initClack() before using prompts");
34
+ return p;
35
+ }
36
+
37
+ /**
38
+ * Print the Effectum banner via clack intro.
39
+ */
40
+ function printBanner() {
41
+ p.intro("EFFECTUM — Autonomous development system for Claude Code");
42
+ }
43
+
44
+ /**
45
+ * Check if the user cancelled a prompt (Ctrl+C).
46
+ * @param {*} value
47
+ */
48
+ function handleCancel(value) {
49
+ if (p.isCancel(value)) {
50
+ p.cancel("Setup cancelled.");
51
+ process.exit(0);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Ask for project name with a detected default.
57
+ * @param {string} detected
58
+ * @returns {Promise<string>}
59
+ */
60
+ async function askProjectName(detected) {
61
+ const value = await p.text({
62
+ message: "Project name",
63
+ placeholder: detected,
64
+ initialValue: detected,
65
+ validate: (v) => {
66
+ if (!v.trim()) return "Project name is required";
67
+ },
68
+ });
69
+ handleCancel(value);
70
+ return value;
71
+ }
72
+
73
+ /**
74
+ * Ask for tech stack selection.
75
+ * @param {string|null} detected - auto-detected stack key
76
+ * @returns {Promise<string>}
77
+ */
78
+ async function askStack(detected) {
79
+ const value = await p.select({
80
+ message: "Tech stack",
81
+ options: STACK_CHOICES.map((c) => ({
82
+ value: c.value,
83
+ label: c.label,
84
+ hint: c.hint,
85
+ })),
86
+ initialValue: detected || "generic",
87
+ });
88
+ handleCancel(value);
89
+ return value;
90
+ }
91
+
92
+ /**
93
+ * Ask for communication language.
94
+ * @returns {Promise<{ language: string, customLanguage?: string }>}
95
+ */
96
+ async function askLanguage() {
97
+ const value = await p.select({
98
+ message: "Communication language",
99
+ options: LANGUAGE_CHOICES.map((c) => ({
100
+ value: c.value,
101
+ label: c.label,
102
+ hint: c.hint,
103
+ })),
104
+ initialValue: "english",
105
+ });
106
+ handleCancel(value);
107
+
108
+ if (value === "custom") {
109
+ const custom = await p.text({
110
+ message: "Enter your language instruction",
111
+ placeholder: 'e.g. "Speak French with the user"',
112
+ validate: (v) => {
113
+ if (!v.trim()) return "Language instruction is required";
114
+ },
115
+ });
116
+ handleCancel(custom);
117
+ return { language: "custom", customLanguage: custom };
118
+ }
119
+
120
+ return { language: value };
121
+ }
122
+
123
+ /**
124
+ * Ask for autonomy level.
125
+ * @returns {Promise<string>}
126
+ */
127
+ async function askAutonomy() {
128
+ const value = await p.select({
129
+ message: "Autonomy level",
130
+ options: AUTONOMY_CHOICES.map((c) => ({
131
+ value: c.value,
132
+ label: c.label,
133
+ hint: c.hint,
134
+ })),
135
+ initialValue: "standard",
136
+ });
137
+ handleCancel(value);
138
+ return value;
139
+ }
140
+
141
+ /**
142
+ * Ask which MCP servers to install via multi-select.
143
+ * @returns {Promise<string[]>}
144
+ */
145
+ async function askMcpServers() {
146
+ const value = await p.multiselect({
147
+ message: "MCP servers (space to toggle, enter to confirm)",
148
+ options: MCP_SERVERS.map((s) => ({
149
+ value: s.key,
150
+ label: s.label,
151
+ hint: s.desc,
152
+ })),
153
+ initialValues: MCP_SERVERS.map((s) => s.key),
154
+ required: false,
155
+ });
156
+ handleCancel(value);
157
+ return value;
158
+ }
159
+
160
+ /**
161
+ * Ask whether to install Playwright browsers.
162
+ * @returns {Promise<boolean>}
163
+ */
164
+ async function askPlaywright() {
165
+ const value = await p.confirm({
166
+ message: "Install Playwright browsers? (required for /e2e)",
167
+ initialValue: true,
168
+ });
169
+ handleCancel(value);
170
+ return value;
171
+ }
172
+
173
+ /**
174
+ * Ask whether to create a new git branch.
175
+ * @returns {Promise<{ create: boolean, name?: string }>}
176
+ */
177
+ async function askGitBranch() {
178
+ const create = await p.confirm({
179
+ message: "Create a new git branch for this setup?",
180
+ initialValue: false,
181
+ });
182
+ handleCancel(create);
183
+
184
+ if (!create) return { create: false };
185
+
186
+ const name = await p.text({
187
+ message: "Branch name",
188
+ initialValue: "effectum-setup",
189
+ placeholder: "effectum-setup",
190
+ });
191
+ handleCancel(name);
192
+ return { create: true, name };
193
+ }
194
+
195
+ /**
196
+ * Display a summary note.
197
+ * @param {object} config
198
+ * @param {string[]} files
199
+ */
200
+ function showSummary(config, files) {
201
+ const lines = [
202
+ `Project: ${config.projectName}`,
203
+ `Stack: ${config.stack}`,
204
+ `Language: ${config.language}`,
205
+ `Autonomy: ${config.autonomyLevel}`,
206
+ `Pkg Mgr: ${config.packageManager}`,
207
+ `Formatter: ${config.formatter}`,
208
+ `MCP: ${(config.mcpServers || []).join(", ") || "none"}`,
209
+ "",
210
+ `Files created/updated:`,
211
+ ...files.map((f) => ` ${f}`),
212
+ ];
213
+ p.note(lines.join("\n"), "Configuration Summary");
214
+ }
215
+
216
+ /**
217
+ * Show the next-steps outro.
218
+ * @param {boolean} isGlobal
219
+ */
220
+ function showOutro(isGlobal) {
221
+ if (isGlobal) {
222
+ p.outro(
223
+ "Effectum ready! In any project, run: npx @aslomon/effectum init",
224
+ );
225
+ } else {
226
+ p.outro(
227
+ "Effectum ready! Open Claude Code here and start building. Try /plan or /prd:new",
228
+ );
229
+ }
230
+ }
231
+
232
+ module.exports = {
233
+ initClack,
234
+ getClack,
235
+ printBanner,
236
+ handleCancel,
237
+ askProjectName,
238
+ askStack,
239
+ askLanguage,
240
+ askAutonomy,
241
+ askMcpServers,
242
+ askPlaywright,
243
+ askGitBranch,
244
+ showSummary,
245
+ showOutro,
246
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared utility functions.
3
+ */
4
+ "use strict";
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ /**
10
+ * Ensure a directory exists (recursive mkdir).
11
+ * @param {string} dir
12
+ */
13
+ function ensureDir(dir) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+
17
+ /**
18
+ * Deep-merge two plain objects. Source wins on conflicts.
19
+ * Arrays are NOT merged — source replaces target.
20
+ * @param {object} target
21
+ * @param {object} source
22
+ * @returns {object}
23
+ */
24
+ function deepMerge(target, source) {
25
+ const out = Object.assign({}, target);
26
+ for (const key of Object.keys(source)) {
27
+ if (
28
+ source[key] &&
29
+ typeof source[key] === "object" &&
30
+ !Array.isArray(source[key]) &&
31
+ out[key] &&
32
+ typeof out[key] === "object" &&
33
+ !Array.isArray(out[key])
34
+ ) {
35
+ out[key] = deepMerge(out[key], source[key]);
36
+ } else {
37
+ out[key] = source[key];
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /**
44
+ * Find the Effectum repo root (parent of bin/).
45
+ * @returns {string}
46
+ */
47
+ function findRepoRoot() {
48
+ const binDir = path.dirname(require.main?.filename || __filename);
49
+ // If we're in bin/lib/, go up two levels; if in bin/, go up one
50
+ if (path.basename(binDir) === "lib") {
51
+ return path.resolve(binDir, "..", "..");
52
+ }
53
+ return path.resolve(binDir, "..");
54
+ }
55
+
56
+ module.exports = { ensureDir, deepMerge, findRepoRoot };
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reconfigure — re-apply settings from .effectum.json.
4
+ * Reads the saved config and regenerates CLAUDE.md, settings.json, guardrails.md.
5
+ */
6
+ "use strict";
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const { readConfig } = require("./lib/config");
11
+ const { loadStackPreset } = require("./lib/stack-parser");
12
+ const {
13
+ buildSubstitutionMap,
14
+ renderTemplate,
15
+ findTemplatePath,
16
+ findRemainingPlaceholders,
17
+ substituteAll,
18
+ } = require("./lib/template");
19
+ const { AUTONOMY_MAP, FORMATTER_MAP } = require("./lib/constants");
20
+ const { ensureDir, deepMerge, findRepoRoot } = require("./lib/utils");
21
+ const { initClack } = require("./lib/ui");
22
+
23
+ // ─── Main ─────────────────────────────────────────────────────────────────
24
+
25
+ async function main() {
26
+ const args = process.argv.slice(2);
27
+ const dryRun = args.includes("--dry-run");
28
+ const targetDir = process.cwd();
29
+ const repoRoot = findRepoRoot();
30
+
31
+ const p = await initClack();
32
+ p.intro("EFFECTUM — Reconfigure");
33
+
34
+ // Read existing config
35
+ const config = readConfig(targetDir);
36
+ if (!config) {
37
+ p.log.error(
38
+ "No .effectum.json found in this directory. Run `npx @aslomon/effectum` first.",
39
+ );
40
+ process.exit(1);
41
+ }
42
+
43
+ p.log.info(`Reconfiguring "${config.projectName}" (${config.stack})`);
44
+
45
+ if (dryRun) {
46
+ p.note(JSON.stringify(config, null, 2), "Current Configuration");
47
+ p.log.info("Dry run — files that would be regenerated:");
48
+ p.log.step(" CLAUDE.md");
49
+ p.log.step(" .claude/settings.json");
50
+ p.log.step(" .claude/guardrails.md");
51
+ p.outro("Dry run complete. No changes made.");
52
+ process.exit(0);
53
+ }
54
+
55
+ const claudeDir = path.join(targetDir, ".claude");
56
+
57
+ // Load stack preset
58
+ const stackSections = loadStackPreset(config.stack, targetDir, repoRoot);
59
+ const vars = buildSubstitutionMap(config, stackSections);
60
+
61
+ const s = p.spinner();
62
+ s.start("Regenerating configuration files...");
63
+
64
+ // 1. CLAUDE.md
65
+ const claudeMdTmpl = findTemplatePath("CLAUDE.md.tmpl", targetDir, repoRoot);
66
+ const { content: claudeMdContent, remaining: claudeMdRemaining } =
67
+ renderTemplate(claudeMdTmpl, vars);
68
+ fs.writeFileSync(path.join(targetDir, "CLAUDE.md"), claudeMdContent, "utf8");
69
+
70
+ if (claudeMdRemaining.length > 0) {
71
+ p.log.warn(
72
+ `CLAUDE.md has remaining placeholders: ${claudeMdRemaining.join(", ")}`,
73
+ );
74
+ }
75
+
76
+ // 2. settings.json
77
+ const settingsTmpl = findTemplatePath(
78
+ "settings.json.tmpl",
79
+ targetDir,
80
+ repoRoot,
81
+ );
82
+ let settingsObj;
83
+ try {
84
+ settingsObj = JSON.parse(fs.readFileSync(settingsTmpl, "utf8"));
85
+ } catch (e) {
86
+ throw new Error(`Could not parse settings template: ${e.message}`);
87
+ }
88
+
89
+ const autonomy = AUTONOMY_MAP[config.autonomyLevel] || AUTONOMY_MAP.standard;
90
+ settingsObj.permissions = {
91
+ ...settingsObj.permissions,
92
+ ...autonomy.permissions,
93
+ defaultMode: autonomy.defaultMode,
94
+ deny: settingsObj.permissions?.deny || [],
95
+ };
96
+
97
+ const formatter = FORMATTER_MAP[config.stack] || FORMATTER_MAP.generic;
98
+ if (settingsObj.hooks?.PostToolUse) {
99
+ for (const group of settingsObj.hooks.PostToolUse) {
100
+ if (group.matcher === "Edit|Write") {
101
+ for (const hook of group.hooks) {
102
+ if (
103
+ hook.command &&
104
+ hook.command.includes("formatter-not-configured")
105
+ ) {
106
+ if (formatter.command === "echo no-formatter-configured") {
107
+ hook.command = "echo no-formatter-configured";
108
+ } else {
109
+ hook.command = `bash -c 'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // empty"); if [[ "$FILE" =~ \\.(${formatter.glob})$ ]]; then ${formatter.command} "$FILE" 2>/dev/null; fi; exit 0'`;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ const settingsDest = path.join(claudeDir, "settings.json");
118
+ let existing = {};
119
+ if (fs.existsSync(settingsDest)) {
120
+ try {
121
+ existing = JSON.parse(fs.readFileSync(settingsDest, "utf8"));
122
+ } catch (_) {}
123
+ }
124
+ const merged = deepMerge(existing, settingsObj);
125
+ ensureDir(path.dirname(settingsDest));
126
+ fs.writeFileSync(
127
+ settingsDest,
128
+ JSON.stringify(merged, null, 2) + "\n",
129
+ "utf8",
130
+ );
131
+
132
+ // 3. guardrails.md
133
+ const guardrailsTmpl = findTemplatePath(
134
+ "guardrails.md.tmpl",
135
+ targetDir,
136
+ repoRoot,
137
+ );
138
+ const guardrailsRaw = fs.readFileSync(guardrailsTmpl, "utf8");
139
+ let guardrailsContent = guardrailsRaw;
140
+
141
+ if (stackSections.STACK_SPECIFIC_GUARDRAILS) {
142
+ guardrailsContent = guardrailsContent.replace(
143
+ /No stack-specific guardrails configured yet\. Run \/setup to configure for your stack\./,
144
+ stackSections.STACK_SPECIFIC_GUARDRAILS,
145
+ );
146
+ }
147
+ if (stackSections.TOOL_SPECIFIC_GUARDRAILS) {
148
+ guardrailsContent = guardrailsContent.replace(
149
+ /No tool-specific guardrails configured yet\. Run \/setup to configure\./,
150
+ stackSections.TOOL_SPECIFIC_GUARDRAILS,
151
+ );
152
+ }
153
+
154
+ const guardrailsDest = path.join(claudeDir, "guardrails.md");
155
+ ensureDir(path.dirname(guardrailsDest));
156
+ fs.writeFileSync(guardrailsDest, guardrailsContent, "utf8");
157
+
158
+ s.stop("Configuration files regenerated");
159
+
160
+ p.log.success("CLAUDE.md — updated");
161
+ p.log.success(".claude/settings.json — updated");
162
+ p.log.success(".claude/guardrails.md — updated");
163
+
164
+ p.outro("Reconfiguration complete!");
165
+ }
166
+
167
+ main().catch((err) => {
168
+ console.error(`Fatal error: ${err.message}`);
169
+ process.exit(1);
170
+ });
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@aslomon/effectum",
3
- "version": "0.1.6",
4
- "description": "Autonomous development system for Claude Code describe what you want, get production-ready code",
3
+ "version": "0.2.1",
4
+ "description": "Autonomous development system for Claude Code \u2014 describe what you want, get production-ready code",
5
5
  "bin": {
6
- "effectum": "bin/install.js"
6
+ "effectum": "bin/effectum.js"
7
7
  },
8
8
  "files": [
9
9
  "bin/",
@@ -25,5 +25,8 @@
25
25
  ],
26
26
  "engines": {
27
27
  "node": ">=18.0.0"
28
+ },
29
+ "dependencies": {
30
+ "@clack/prompts": "^1.1.0"
28
31
  }
29
- }
32
+ }