@esneiderbravo/speclaw 2.0.14 → 2.0.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.
@@ -24,17 +24,16 @@ export function detectProjectName(cwd) {
24
24
  return path.basename(cwd);
25
25
  }
26
26
  /**
27
- * Interactive setup: pick agents (and packs when any exist), scaffold, index,
27
+ * Setup: create `.agents/` (plus any `--agents`), pick packs when any exist, scaffold, index,
28
28
  * and print the handoff prompt.
29
29
  *
30
- * @param flags - Parsed flags; runs interactively on a TTY unless `--agents`, `--yes`, or
31
- * `-y` is set. Honors `--project-name`, `--packs`, and `--no-index`.
30
+ * @param flags - Parsed flags; only the pack picker is interactive (on a TTY unless
31
+ * `--agents`, `--yes`, or `-y` is set). Honors `--project-name`, `--packs`, and `--no-index`.
32
32
  */
33
33
  export async function runInit(flags) {
34
34
  const cwd = process.cwd();
35
35
  const projectName = flags["project-name"] || detectProjectName(cwd);
36
36
  const interactive = Boolean(process.stdin.isTTY) && !flags.agents && !flags.yes && !flags.y;
37
- let agents;
38
37
  let packs;
39
38
  const availablePacks = loadPacks();
40
39
  const packIds = Object.keys(availablePacks);
@@ -50,36 +49,26 @@ export async function runInit(flags) {
50
49
  `Later, ${ui.code("speclaw update")} upgrades itself through npx.`);
51
50
  ui.plain();
52
51
  }
53
- if (interactive) {
54
- // Covers: req~remove-agents-pack~1
55
- const prompts = {
56
- agents: () => clack.multiselect({
57
- message: "Which agents do you use? (space to select)",
58
- options: AGENTS.map((a) => ({ value: a.id, label: a.label })),
59
- initialValues: ["agents"],
60
- required: true,
61
- }),
62
- };
63
- if (packIds.length > 0) {
64
- prompts.packs = () => clack.multiselect({
65
- message: "Which tool packs to install?",
66
- options: packIds.map((id) => ({
67
- value: id,
68
- label: id,
69
- hint: availablePacks[id].description.slice(0, 50),
70
- })),
71
- initialValues: [],
72
- required: false,
73
- });
74
- }
75
- const answers = await clack.group(prompts, { onCancel: () => process.exit(1) });
76
- agents = answers.agents;
77
- packs = answers.packs ?? [];
78
- }
79
- else {
80
- // Default: one agent-facing folder, `.agents/`, linked into ai-specs/.
81
- agents = list(flags.agents).length ? list(flags.agents) : ["agents"];
82
- packs = list(flags.packs);
52
+ // Agents are never prompted for: init creates one agent-facing folder,
53
+ // `.agents/`, linked into ai-specs/. Other IDE folders only via `--agents`
54
+ // or `speclaw agent add`.
55
+ const agents = list(flags.agents).length ? list(flags.agents) : ["agents"];
56
+ packs = list(flags.packs);
57
+ // Covers: req~remove-agents-pack~1
58
+ if (interactive && packIds.length > 0 && !packs.length) {
59
+ const answer = await clack.multiselect({
60
+ message: "Which tool packs to install?",
61
+ options: packIds.map((id) => ({
62
+ value: id,
63
+ label: id,
64
+ hint: availablePacks[id].description.slice(0, 50),
65
+ })),
66
+ initialValues: [],
67
+ required: false,
68
+ });
69
+ if (clack.isCancel(answer))
70
+ process.exit(1);
71
+ packs = answer;
83
72
  }
84
73
  const unknownAgents = agents.filter((a) => !agentById(a));
85
74
  if (unknownAgents.length) {
@@ -13,7 +13,7 @@ Install globally so the command is always available:
13
13
  npm i -g @esneiderbravo/speclaw (or run npx @esneiderbravo/speclaw@latest init)
14
14
 
15
15
  Setup
16
- init Interactive setup: pick agents, scaffold, index, get the prompt
16
+ init Setup: .agents/ linked into ai-specs/, scaffold, index, get the prompt
17
17
  (--minimal omits setup/lifecycle MCP tools)
18
18
  update Re-run itself at the latest version via npx, then apply project
19
19
  migrations (--check reports version only; --no-self-update
@@ -123,8 +123,9 @@ you rarely run it by hand.
123
123
  name: "init",
124
124
  usage: `Usage: speclaw init [options]
125
125
 
126
- Interactive setup: pick agents, scaffold the constitution and lawbook, build the
127
- Compass index, and print the starting prompt. Prefer
126
+ Setup without an agent picker: create .agents/ linked into ai-specs/ (gitignored),
127
+ scaffold the constitution and lawbook, build the Compass index, and print the
128
+ starting prompt. Prefer
128
129
  npx @esneiderbravo/speclaw@latest init so the scaffold matches the latest release.
129
130
 
130
131
  Options
@@ -11,6 +11,7 @@ export const AGENTS = [
11
11
  linkTargets: ["skills", "commands", "agents"],
12
12
  mcpFile: ".mcp.json",
13
13
  hooks: { file: ".claude/settings.json", key: "hooks" },
14
+ ownedPaths: [".claude/rules/speclaw"],
14
15
  },
15
16
  {
16
17
  id: "cursor",
@@ -141,11 +142,23 @@ export function configureAgent(projectPath, agentId, report) {
141
142
  if (!agent)
142
143
  throw new Error(`Unknown agent "${agentId}". Known: ${AGENTS.map((a) => a.id).join(", ")}`);
143
144
  const ideRoot = path.join(projectPath, agent.ideDir);
144
- // An agent folder speclaw creates holds only links into the gitignored
145
- // ai-specs/ and per-developer wiring: keep it out of version control. A
146
- // folder that already existed is the user's and is left as it is.
145
+ // Everything speclaw puts in an agent folder is links into the gitignored
146
+ // ai-specs/ plus per-developer wiring: keep it out of version control. A
147
+ // folder speclaw creates is ignored whole; in a folder that already existed
148
+ // (the IDE's own, e.g. `.claude/settings.local.json`) only speclaw's entries are.
149
+ const comment = `speclaw ${agent.label} links (regenerated by init; never commit)`;
147
150
  if (!fs.existsSync(ideRoot)) {
148
- ensureGitignore(projectPath, `${agent.ideDir}/`, `speclaw ${agent.label} links (regenerated by init; never commit)`, report);
151
+ ensureGitignore(projectPath, `${agent.ideDir}/`, comment, report);
152
+ }
153
+ else {
154
+ const owned = agent.linkTargets
155
+ .filter((t) => fs.existsSync(path.join(projectPath, "ai-specs", t)))
156
+ .map((t) => `${agent.ideDir}/${t}`)
157
+ .concat(agent.ownedPaths ?? []);
158
+ if (agent.hooks && !fs.existsSync(path.join(projectPath, agent.hooks.file)))
159
+ owned.push(agent.hooks.file);
160
+ for (const entry of owned)
161
+ ensureGitignore(projectPath, entry, comment, report);
149
162
  }
150
163
  for (const target of agent.linkTargets) {
151
164
  const sourceDir = path.join(projectPath, "ai-specs", target);
@@ -164,13 +177,25 @@ export function configureAgent(projectPath, agentId, report) {
164
177
  writeMcpConfig(projectPath, agent.mcpFile, report);
165
178
  }
166
179
  /**
167
- * List the agents already set up in this project (their IDE dir exists).
180
+ * List the agents speclaw set up in this project: those whose IDE dir holds at
181
+ * least one speclaw link into ai-specs/. A bare IDE dir is not enough — Claude
182
+ * Code and Cursor create `.claude/` / `.cursor/` on their own, and treating
183
+ * that as "configured" made `update` populate folders nobody chose.
168
184
  *
169
185
  * @param projectPath - Project root to inspect.
170
- * @returns The ids of agents whose IDE directory is present.
186
+ * @returns The ids of agents with a speclaw link in their IDE directory.
171
187
  */
172
188
  export function detectConfiguredAgents(projectPath) {
173
- return AGENTS.filter((a) => fs.existsSync(path.join(projectPath, a.ideDir))).map((a) => a.id);
189
+ return AGENTS.filter((a) => a.linkTargets.some((t) => isAiSpecsLink(path.join(projectPath, a.ideDir, t), t))).map((a) => a.id);
190
+ }
191
+ /** True when `p` is a symlink whose target ends in `ai-specs/<target>`. */
192
+ function isAiSpecsLink(p, target) {
193
+ try {
194
+ return fs.readlinkSync(p).replace(/\\/g, "/").endsWith(`ai-specs/${target}`);
195
+ }
196
+ catch {
197
+ return false;
198
+ }
174
199
  }
175
200
  /**
176
201
  * Re-run configuration for every already-configured agent (e.g. after adding a pack).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esneiderbravo/speclaw",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },