@constraint/cli 0.3.3 → 0.3.4

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 CHANGED
@@ -55,9 +55,11 @@ Or target agents explicitly:
55
55
  constraint setup --agent codex --agent claude-code
56
56
  ```
57
57
 
58
- Setup installs the `constraint-skills` companion skill and offers to add a
59
- marked Constraint block to the agent's global instructions. Existing
60
- `AGENTS.md` and `CLAUDE.md` content outside that block is preserved.
58
+ Setup presents two separate stages: first it offers to add a marked Constraint
59
+ block to each agent's global instructions, then it shows the global destination
60
+ and installs the `constraint-skills` companion using the regular install-plan
61
+ flow. Existing `AGENTS.md` and `CLAUDE.md` content outside that block is
62
+ preserved.
61
63
 
62
64
  Preview the instruction change without applying it:
63
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constraint/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Install, publish, and report Agent Skills with Constraint",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@ constraint setup
12
12
  constraint doctor
13
13
  ```
14
14
 
15
- `login` uses WorkOS device authorization. Never request or embed an API key or client secret. `setup` detects Codex and Claude Code, installs this skill, and deterministically merges a marked global instruction block without replacing unrelated instructions.
15
+ `login` uses WorkOS device authorization. Never request or embed an API key or client secret. `setup` detects Codex and Claude Code, then separately presents the managed global-instruction change and this skill's global installation destinations. It never replaces unrelated instructions.
16
16
 
17
17
  ## Discover, inspect, install, and update
18
18
 
package/src/cli.js CHANGED
@@ -18,7 +18,12 @@ import {
18
18
  note,
19
19
  taskSpinner,
20
20
  } from './prompts.js';
21
- import { setupAgent, setupStatus } from './setup.js';
21
+ import {
22
+ applyInstructionSetup,
23
+ installCompanionSkill,
24
+ setupPlan,
25
+ setupStatus,
26
+ } from './setup.js';
22
27
  import {
23
28
  configuredAgents,
24
29
  installSkill,
@@ -30,7 +35,7 @@ import {
30
35
  uploadSkill,
31
36
  } from './skills.js';
32
37
 
33
- const VERSION = '0.3.3';
38
+ const VERSION = '0.3.4';
34
39
 
35
40
  const HELP = `Constraint Skills CLI
36
41
 
@@ -236,17 +241,78 @@ async function setupCommand(args, out) {
236
241
  agents = useUi && !yes ? await chooseAgents(detected) : detected;
237
242
  }
238
243
  if (!agents.length) throw new Error('Choose at least one agent with --agent.');
239
- const results = [];
240
- for (const agent of agents) {
241
- results.push(await setupAgent(agent, {
242
- dryRun,
243
- yes,
244
- output: useUi ? () => {} : out.line,
245
- confirmChange: confirmPlan,
246
- }));
244
+ const plans = await Promise.all(agents.map(setupPlan));
245
+ const conflicts = plans.filter((plan) => plan.skill.action === 'conflict');
246
+ if (conflicts.length) {
247
+ throw new Error(`Local Constraint skill has uncommitted changes and was not replaced: ${conflicts.map((plan) => plan.skill.path).join(', ')}`);
248
+ }
249
+ if (dryRun) {
250
+ out.data(plans.map((plan) => ({
251
+ agent: plan.agent,
252
+ instructions: {
253
+ path: plan.instructions.path,
254
+ action: plan.instructions.changed ? 'update' : 'current',
255
+ content: plan.instructions.content,
256
+ },
257
+ skill: { path: plan.skill.path, scope: 'global', action: plan.skill.action },
258
+ })));
259
+ return;
260
+ }
261
+ const hasInstructionChanges = plans.some((plan) => plan.instructions.changed);
262
+ const hasSkillChanges = plans.some((plan) => plan.skill.action !== 'current');
263
+ if (!useUi && !yes && (hasInstructionChanges || hasSkillChanges)) {
264
+ throw new Error('Non-interactive setup has pending changes. Pass --yes or use --dry-run.');
265
+ }
266
+
267
+ let configureInstructions = true;
268
+ let installSkill = true;
269
+ if (useUi) {
270
+ note(
271
+ plans.map((plan) => `${humanize(plan.agent)}\n${plan.instructions.path}\n${plan.instructions.changed ? 'Managed block will be added or updated' : 'Already configured'}`).join('\n\n'),
272
+ '1. Configure agent instructions',
273
+ );
274
+ configureInstructions = !hasInstructionChanges || yes || await confirmPlan('Apply these instruction changes?');
275
+
276
+ note(
277
+ [
278
+ 'Skill: constraint-skills',
279
+ 'Scope: Global',
280
+ '',
281
+ ...plans.flatMap((plan) => [
282
+ `${humanize(plan.agent)} destination:`,
283
+ `${plan.skill.path} · ${plan.skill.action === 'current' ? 'already current' : plan.skill.action}`,
284
+ '',
285
+ ]),
286
+ ].join('\n').trim(),
287
+ '2. Install Constraint Skills',
288
+ );
289
+ installSkill = !hasSkillChanges || yes || await confirmPlan(`Install globally for ${agents.map(humanize).join(' and ')}?`);
247
290
  }
248
- if (useUi) finish(`Constraint Skills configured for ${agents.join(' and ')}.`);
249
- else out.data(results);
291
+
292
+ const result = { instructions: [], skill: [] };
293
+ if (configureInstructions) {
294
+ const spinner = taskSpinner(useUi && hasInstructionChanges);
295
+ for (const plan of plans) {
296
+ spinner.message(`Configuring ${humanize(plan.agent)} instructions`);
297
+ result.instructions.push(await applyInstructionSetup(plan.agent));
298
+ }
299
+ spinner.stop('Agent instructions configured.');
300
+ }
301
+ if (installSkill) {
302
+ const spinner = taskSpinner(useUi && hasSkillChanges);
303
+ for (const plan of plans) {
304
+ spinner.message(`Installing Constraint Skills for ${humanize(plan.agent)}`);
305
+ result.skill.push(await installCompanionSkill(plan.agent));
306
+ }
307
+ spinner.stop('Constraint Skills installed globally.');
308
+ }
309
+ if (useUi) {
310
+ finish([
311
+ 'Setup complete.',
312
+ `Instructions: ${configureInstructions ? `${agents.length} configured` : 'skipped'}`,
313
+ `Skill: ${installSkill ? `installed for ${agents.map(humanize).join(' and ')}` : 'skipped'}`,
314
+ ].join('\n'));
315
+ } else out.data(result);
250
316
  }
251
317
 
252
318
  async function doctorCommand(args, config, out) {
package/src/setup.js CHANGED
@@ -42,41 +42,107 @@ async function companionFiles() {
42
42
  return readSkillDirectory(path.resolve(here, '..', 'skills', 'constraint-skills'));
43
43
  }
44
44
 
45
- export async function setupAgent(agent, {
46
- dryRun = false,
47
- yes = false,
48
- confirmChange = async () => false,
49
- output = console.log,
50
- } = {}) {
51
- const selected = normalizeAgent(agent);
52
- const paths = agentPaths(selected);
45
+ async function instructionPlan(selected, paths) {
53
46
  let existing = '';
54
- let instructionMode = 0o644;
47
+ let mode = 0o644;
55
48
  try {
56
49
  existing = await fs.readFile(paths.instructions, 'utf8');
57
- instructionMode = (await fs.stat(paths.instructions)).mode & 0o777;
50
+ mode = (await fs.stat(paths.instructions)).mode & 0o777;
58
51
  } catch (error) {
59
52
  if (error && error.code !== 'ENOENT') throw error;
60
53
  }
61
- const merged = mergeInstructionBlock(existing);
62
- output(`${selected}: ${paths.instructions}`);
63
- if (dryRun) {
64
- output(merged);
65
- return { agent: selected, changed: merged !== existing, applied: false };
66
- }
67
- if (merged !== existing && !yes && !(await confirmChange(`Add the Constraint Skills block to ${paths.instructions}?`))) {
68
- return { agent: selected, changed: true, applied: false };
54
+ const content = mergeInstructionBlock(existing);
55
+ return { path: paths.instructions, changed: content !== existing, content, mode };
56
+ }
57
+
58
+ async function skillPlan(selected, paths) {
59
+ const files = await companionFiles();
60
+ const bundledHash = packageDigest(files);
61
+ const config = await loadConfig();
62
+ const expectedHash = config.companion_hashes?.[selected] || null;
63
+ const target = path.join(paths.skills, 'constraint-skills');
64
+ let currentHash = null;
65
+ try {
66
+ currentHash = packageDigest(await readSkillDirectory(target));
67
+ } catch (error) {
68
+ if (error && error.code !== 'ENOENT') throw error;
69
69
  }
70
+ const action = currentHash === bundledHash
71
+ ? 'current'
72
+ : currentHash === null
73
+ ? 'install'
74
+ : expectedHash && currentHash === expectedHash
75
+ ? 'update'
76
+ : 'conflict';
77
+ return { path: target, action, bundledHash, expectedHash };
78
+ }
79
+
80
+ export async function setupPlan(agent) {
81
+ const selected = normalizeAgent(agent);
82
+ const paths = agentPaths(selected);
83
+ return {
84
+ agent: selected,
85
+ instructions: await instructionPlan(selected, paths),
86
+ skill: await skillPlan(selected, paths),
87
+ };
88
+ }
89
+
90
+ async function recordConfiguredAgent(selected, config = null) {
91
+ const next = config || await loadConfig();
92
+ next.agents = [...new Set([...next.agents.map(normalizeAgent), selected])];
93
+ await saveConfig(next);
94
+ }
95
+
96
+ export async function applyInstructionSetup(agent) {
97
+ const selected = normalizeAgent(agent);
98
+ const paths = agentPaths(selected);
99
+ const plan = await instructionPlan(selected, paths);
100
+ if (plan.changed) await writeInstructions(plan.path, plan.content, plan.mode);
101
+ await recordConfiguredAgent(selected);
102
+ return { agent: selected, path: plan.path, changed: plan.changed, applied: true };
103
+ }
104
+
105
+ export async function installCompanionSkill(agent) {
106
+ const selected = normalizeAgent(agent);
107
+ const paths = agentPaths(selected);
70
108
  const files = await companionFiles();
71
109
  const config = await loadConfig();
72
110
  config.companion_hashes ||= {};
73
111
  const target = path.join(paths.skills, 'constraint-skills');
74
- await writeSkillDirectory(target, files, { expectedHash: config.companion_hashes[selected] || null });
75
- if (merged !== existing) await writeInstructions(paths.instructions, merged, instructionMode);
76
- config.companion_hashes[selected] = packageDigest(files);
77
- config.agents = [...new Set([...config.agents.map(normalizeAgent), selected])];
78
- await saveConfig(config);
79
- return { agent: selected, changed: merged !== existing, applied: true };
112
+ const result = await writeSkillDirectory(target, files, {
113
+ expectedHash: config.companion_hashes[selected] || null,
114
+ });
115
+ config.companion_hashes[selected] = result.hash;
116
+ await recordConfiguredAgent(selected, config);
117
+ return { agent: selected, path: target, changed: result.changed, applied: true };
118
+ }
119
+
120
+ export async function setupAgent(agent, {
121
+ dryRun = false,
122
+ yes = false,
123
+ confirmChange = async () => false,
124
+ output = console.log,
125
+ instructions = true,
126
+ skill = true,
127
+ } = {}) {
128
+ const selected = normalizeAgent(agent);
129
+ const plan = await setupPlan(selected);
130
+ output(`${selected}: ${plan.instructions.path}`);
131
+ if (dryRun) {
132
+ if (instructions) output(plan.instructions.content);
133
+ if (skill) output(`Install constraint-skills globally at ${plan.skill.path} (${plan.skill.action}).`);
134
+ return { agent: selected, instructions: plan.instructions, skill: plan.skill, applied: false };
135
+ }
136
+ let instructionResult = null;
137
+ let skillResult = null;
138
+ if (instructions) {
139
+ const approved = !plan.instructions.changed || yes || await confirmChange(
140
+ `Add the Constraint Skills block to ${plan.instructions.path}?`,
141
+ );
142
+ if (approved) instructionResult = await applyInstructionSetup(selected);
143
+ }
144
+ if (skill) skillResult = await installCompanionSkill(selected);
145
+ return { agent: selected, instructions: instructionResult, skill: skillResult, applied: true };
80
146
  }
81
147
 
82
148
  export async function setupStatus(agent) {