deweyou-cli 0.3.1 → 0.4.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-05-17
4
+
5
+ ### Added
6
+
7
+ - add interactive rule install prompts
8
+ - wire rule installs into init
9
+ - add rule install adapters
10
+ - parse rule install init flags
11
+
12
+ ### Fixed
13
+
14
+ - refuse agents symlink writes
15
+ - harden rule install safety
16
+ - preserve explicit init mode
17
+ - preserve init flags in wizard
18
+ - omit claude preview without rules
19
+ - include agents preview for claude installs
20
+ - read inline project rules from cache
21
+ - forward init rule install flags
22
+ - handle Claude rule install validation
23
+ - preserve only AGENTS Claude symlink
24
+ - document rule install init flags
25
+
26
+ ### Changed
27
+
28
+ - share managed markdown sections
29
+
30
+ ### Documentation
31
+
32
+ - document rule install targets
3
33
  ## 0.3.1 - 2026-05-17
4
34
 
5
35
  ### Changed
package/README.md CHANGED
@@ -91,6 +91,12 @@ Initializes the current repository with selected skills and rules.
91
91
  deweyou-cli agent init
92
92
  ```
93
93
 
94
+ Usage:
95
+
96
+ ```text
97
+ deweyou-cli agent init [--all] [--skills a,b] [--rules a,b] [--mode link|copy|pointer] [--scope project|global] [--tools codex,claude|all] [--rule-wiring reference|inline] [--yes] [--dry-run] [--force]
98
+ ```
99
+
94
100
  Without selection flags, this opens an interactive setup where you choose:
95
101
 
96
102
  - install mode
@@ -102,6 +108,8 @@ Scripted examples:
102
108
  ```bash
103
109
  deweyou-cli agent init --all --mode link --yes
104
110
  deweyou-cli agent init --skills repo-memory,spec-driven-coding,git-delivery --rules code-style
111
+ deweyou-cli agent init --scope project --tools codex,claude --rules code-style --mode link
112
+ deweyou-cli agent init --scope global --tools all --rules code-style --rule-wiring reference --yes
105
113
  deweyou-cli agent init --dry-run
106
114
  ```
107
115
 
@@ -182,6 +190,10 @@ AGENTS.md
182
190
  `AGENTS.md` receives a managed Dewey section that points agents at the selected
183
191
  workflow context. Existing content outside that managed section is preserved.
184
192
 
193
+ Project installs write repository instruction files such as `AGENTS.md` and
194
+ `CLAUDE.md`. Global installs write user-level instruction files such as
195
+ `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md`.
196
+
185
197
  ## Safety Notes
186
198
 
187
199
  - Run `deweyou-cli agent update` before `deweyou-cli agent init`.
package/dist/deweyou.mjs CHANGED
@@ -11,7 +11,10 @@ const VALUE_FLAGS = new Set([
11
11
  "mode",
12
12
  "skills",
13
13
  "rules",
14
- "format"
14
+ "format",
15
+ "scope",
16
+ "tools",
17
+ "rule-wiring"
15
18
  ]);
16
19
  const FLAGS_BY_COMMAND = {
17
20
  init: new Set([
@@ -19,6 +22,9 @@ const FLAGS_BY_COMMAND = {
19
22
  "skills",
20
23
  "rules",
21
24
  "mode",
25
+ "scope",
26
+ "tools",
27
+ "rule-wiring",
22
28
  "yes",
23
29
  "dry-run",
24
30
  "force"
@@ -69,7 +75,7 @@ function isAllowedForCommand(command, name) {
69
75
  return FLAGS_BY_COMMAND[command]?.has(name) ?? false;
70
76
  }
71
77
  function parseValue(name, value) {
72
- if (name === "skills" || name === "rules") return value.split(",").map((item) => item.trim()).filter(Boolean);
78
+ if (name === "skills" || name === "rules" || name === "tools") return value.split(",").map((item) => item.trim()).filter(Boolean);
73
79
  return value;
74
80
  }
75
81
  function toCamel(value) {
@@ -81,7 +87,7 @@ async function main(argv) {
81
87
  const parsed = parseArgs(argv);
82
88
  if (parsed.topic !== "agent") printUsageAndThrow();
83
89
  if (parsed.command === "init") {
84
- const { runInit } = await import("./init-g2D-URoL.mjs");
90
+ const { runInit } = await import("./init-6406tZjR.mjs");
85
91
  await runInit(parsed.flags);
86
92
  return;
87
93
  }
@@ -104,7 +110,7 @@ async function main(argv) {
104
110
  }
105
111
  function usage() {
106
112
  return `Usage:
107
- deweyou-cli agent init [--all] [--skills a,b] [--rules a,b] [--mode link|copy|pointer] [--yes] [--dry-run] [--force]
113
+ deweyou-cli agent init [--all] [--skills a,b] [--rules a,b] [--mode link|copy|pointer] [--scope project|global] [--tools codex,claude|all] [--rule-wiring reference|inline] [--yes] [--dry-run] [--force]
108
114
  deweyou-cli agent update
109
115
  deweyou-cli agent context [--format markdown|json]
110
116
  deweyou-cli agent doctor`;
@@ -1,18 +1,37 @@
1
1
  import { cachePaths, n as writeJson, t as readJson } from "./cache-rbXm6Wyh.mjs";
2
- import { cp, lstat, mkdir, readFile, realpath, rename, rm, symlink, writeFile } from "node:fs/promises";
2
+ import { cp, lstat, mkdir, readFile, readlink, realpath, rename, rm, symlink, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
- import { basename, dirname, isAbsolute, join, relative } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ //#region src/cli/managed-section.ts
6
+ function upsertManagedSection(contents, { start, end, body }) {
7
+ const section = `${start}\n${body.trimEnd()}\n${end}`;
8
+ const markedSection = new RegExp(`${escapeRegex(start)}[\\s\\S]*?${escapeRegex(end)}`);
9
+ if (markedSection.test(contents)) return ensureTrailingNewline(contents.replace(markedSection, section));
10
+ const trimmed = contents.trimEnd();
11
+ if (!trimmed) return `${section}\n`;
12
+ return `${trimmed}\n\n${section}\n`;
13
+ }
14
+ function escapeRegex(value) {
15
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16
+ }
17
+ function ensureTrailingNewline(value) {
18
+ return value.endsWith("\n") ? value : `${value}\n`;
19
+ }
20
+ //#endregion
5
21
  //#region src/cli/agents-md.ts
6
22
  const DEWEYOU_SECTION_START = "<!-- deweyou-agent:start -->";
7
23
  const DEWEYOU_SECTION_END = "<!-- deweyou-agent:end -->";
8
- const DEWEY_SECTION = `${DEWEYOU_SECTION_START}
9
- ## Dewey Workflow
24
+ const DEWEY_SECTION_BODY = `## Dewey Workflow
10
25
 
11
- This repository uses Dewey's personal agent workflow. Inspect \`.agents/\` before making changes, then run \`deweyou-cli agent context --format markdown\` and follow the returned rules, skill index, asset paths, and runtime notices.
12
- ${DEWEYOU_SECTION_END}`;
26
+ This repository uses Dewey's personal agent workflow. Inspect \`.agents/\` before making changes, then run \`deweyou-cli agent context --format markdown\` and follow the returned rules, skill index, asset paths, and runtime notices.`;
13
27
  async function upsertAgentsSection(repoRoot) {
14
28
  const path = join(repoRoot, "AGENTS.md");
15
- const next = upsertSection(await readAgentsFile(path));
29
+ await validateAgentsWritePath(path);
30
+ const next = upsertManagedSection(await readAgentsFile(path), {
31
+ start: DEWEYOU_SECTION_START,
32
+ end: DEWEYOU_SECTION_END,
33
+ body: DEWEY_SECTION_BODY
34
+ });
16
35
  await writeFile(path, next);
17
36
  return next;
18
37
  }
@@ -25,18 +44,160 @@ async function readAgentsFile(path) {
25
44
  throw error;
26
45
  }
27
46
  }
28
- function upsertSection(contents) {
29
- const markedSection = new RegExp(`${escapeRegex(DEWEYOU_SECTION_START)}[\\s\\S]*?${escapeRegex(DEWEYOU_SECTION_END)}`);
30
- if (markedSection.test(contents)) return ensureTrailingNewline(contents.replace(markedSection, DEWEY_SECTION));
31
- const trimmed = contents.trimEnd();
32
- if (!trimmed) return `${DEWEY_SECTION}\n`;
33
- return `${trimmed}\n\n${DEWEY_SECTION}\n`;
47
+ async function validateAgentsWritePath(path) {
48
+ try {
49
+ if ((await lstat(path)).isSymbolicLink()) throw new Error(`Refusing to write Dewey workflow through symlink: ${path}`);
50
+ } catch (error) {
51
+ if (!(error instanceof Error) || !("code" in error)) throw error;
52
+ if (error.code === "ENOENT") return;
53
+ throw error;
54
+ }
34
55
  }
35
- function escapeRegex(value) {
36
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56
+ //#endregion
57
+ //#region src/cli/rule-install.ts
58
+ const CODEX_START = "<!-- deweyou-codex-rules:start -->";
59
+ const CODEX_END = "<!-- deweyou-codex-rules:end -->";
60
+ const CLAUDE_START = "<!-- deweyou-claude-rules:start -->";
61
+ const CLAUDE_END = "<!-- deweyou-claude-rules:end -->";
62
+ async function planRuleInstall(input) {
63
+ validateRuleInstallInput(input);
64
+ if (input.selectedRules.length === 0) return {
65
+ files: [],
66
+ operations: []
67
+ };
68
+ const operations = [];
69
+ if (input.tools.includes("codex")) operations.push(await codexOperation(input));
70
+ if (input.tools.includes("claude")) {
71
+ const operation = await claudeOperation(input);
72
+ if (operation) operations.push(operation);
73
+ }
74
+ return {
75
+ files: operations.map((operation) => operation.path),
76
+ operations
77
+ };
37
78
  }
38
- function ensureTrailingNewline(value) {
39
- return value.endsWith("\n") ? value : `${value}\n`;
79
+ async function applyRuleInstall(plan) {
80
+ for (const operation of plan.operations) {
81
+ await mkdir(dirname(operation.path), { recursive: true });
82
+ await validateInstructionWritePath(operation.path);
83
+ const next = upsertManagedSection(await readTextIfPresent(operation.path), operation);
84
+ await writeFile(operation.path, next);
85
+ }
86
+ }
87
+ async function codexOperation(input) {
88
+ return {
89
+ path: input.scope === "project" ? join(input.repoRoot, "AGENTS.md") : join(input.homeDir, ".codex", "AGENTS.md"),
90
+ start: CODEX_START,
91
+ end: CODEX_END,
92
+ body: await renderRuleSection(input, "Codex")
93
+ };
94
+ }
95
+ async function claudeOperation(input) {
96
+ if (input.scope === "project") {
97
+ const claudePath = join(input.repoRoot, "CLAUDE.md");
98
+ if (await isSymlinkToAgentsMd(claudePath, input.repoRoot)) {
99
+ if (input.tools.includes("codex")) return null;
100
+ return {
101
+ path: join(input.repoRoot, "AGENTS.md"),
102
+ start: CLAUDE_START,
103
+ end: CLAUDE_END,
104
+ body: await renderRuleSection(input, "Claude Code")
105
+ };
106
+ }
107
+ if (input.tools.includes("codex") && !await exists(claudePath)) return {
108
+ path: claudePath,
109
+ start: CLAUDE_START,
110
+ end: CLAUDE_END,
111
+ body: "@AGENTS.md"
112
+ };
113
+ return {
114
+ path: claudePath,
115
+ start: CLAUDE_START,
116
+ end: CLAUDE_END,
117
+ body: await renderRuleSection(input, "Claude Code")
118
+ };
119
+ }
120
+ return {
121
+ path: join(input.homeDir, ".claude", "CLAUDE.md"),
122
+ start: CLAUDE_START,
123
+ end: CLAUDE_END,
124
+ body: await renderRuleSection(input, "Claude Code")
125
+ };
126
+ }
127
+ function validateRuleInstallInput(input) {
128
+ if (input.scope !== "project" && input.scope !== "global") throw new Error(`Invalid rule install scope: ${input.scope}`);
129
+ for (const tool of input.tools) if (tool !== "codex" && tool !== "claude") throw new Error(`Invalid rule install tool: ${tool}`);
130
+ if (input.ruleWiring !== "reference" && input.ruleWiring !== "inline") throw new Error(`Invalid rule wiring: ${input.ruleWiring}`);
131
+ if (input.scope === "global") for (const rule of input.selectedRules) {
132
+ const path = requireRulePath(input, rule);
133
+ if (!isPathInside(input.cacheRoot, path)) throw new Error(`Global Dewey rule path must be inside cacheRoot for ${rule}: ${path}`);
134
+ }
135
+ }
136
+ async function renderRuleSection(input, toolLabel) {
137
+ if (input.ruleWiring === "inline") return `## Dewey Rules for ${toolLabel}
138
+
139
+ Follow these selected Dewey rules:
140
+
141
+ ${(await Promise.all(input.selectedRules.map(async (rule) => {
142
+ return `### ${rule}\n\n${await readFile(requireRulePath(input, rule), "utf8")}`;
143
+ }))).join("\n\n")}`;
144
+ return `## Dewey Rules for ${toolLabel}
145
+
146
+ Follow these selected Dewey rules. Read the referenced files before applying a rule:
147
+
148
+ ${input.selectedRules.map((rule) => `- ${rule}: ${displayPath(input, requireRulePath(input, rule))}`).join("\n")}`;
149
+ }
150
+ function requireRulePath(input, rule) {
151
+ const path = input.rulePaths.get(rule);
152
+ if (!path) throw new Error(`Missing path for Dewey rule: ${rule}`);
153
+ return path;
154
+ }
155
+ function displayPath(input, path) {
156
+ if (input.scope === "project") return relative(input.repoRoot, path);
157
+ return path;
158
+ }
159
+ function isPathInside(root, path) {
160
+ const pathFromRoot = relative(resolve(root), resolve(path));
161
+ return pathFromRoot === "" || !pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot);
162
+ }
163
+ async function readTextIfPresent(path) {
164
+ try {
165
+ return await readFile(path, "utf8");
166
+ } catch (error) {
167
+ if (!(error instanceof Error) || !("code" in error)) throw error;
168
+ if (error.code === "ENOENT") return "";
169
+ throw error;
170
+ }
171
+ }
172
+ async function validateInstructionWritePath(path) {
173
+ try {
174
+ if ((await lstat(path)).isSymbolicLink()) throw new Error(`Refusing to write Dewey rules through symlink: ${path}`);
175
+ } catch (error) {
176
+ if (!(error instanceof Error) || !("code" in error)) throw error;
177
+ if (error.code === "ENOENT") return;
178
+ throw error;
179
+ }
180
+ }
181
+ async function exists(path) {
182
+ try {
183
+ await lstat(path);
184
+ return true;
185
+ } catch (error) {
186
+ if (!(error instanceof Error) || !("code" in error)) throw error;
187
+ if (error.code === "ENOENT") return false;
188
+ throw error;
189
+ }
190
+ }
191
+ async function isSymlinkToAgentsMd(path, repoRoot) {
192
+ try {
193
+ if (!(await lstat(path)).isSymbolicLink()) return false;
194
+ const target = await readlink(path);
195
+ return target === "AGENTS.md" || resolve(dirname(path), target) === resolve(repoRoot, "AGENTS.md");
196
+ } catch (error) {
197
+ if (!(error instanceof Error) || !("code" in error)) throw error;
198
+ if (error.code === "ENOENT") return false;
199
+ throw error;
200
+ }
40
201
  }
41
202
  //#endregion
42
203
  //#region src/cli/init.ts
@@ -45,9 +206,23 @@ const VALID_MODES = new Set([
45
206
  "copy",
46
207
  "pointer"
47
208
  ]);
209
+ const VALID_SCOPES = new Set(["project", "global"]);
210
+ const VALID_TOOLS = new Set(["codex", "claude"]);
211
+ const VALID_TOOL_SELECTIONS = new Set([
212
+ "all",
213
+ "codex",
214
+ "claude"
215
+ ]);
216
+ const VALID_RULE_WIRING = new Set(["reference", "inline"]);
48
217
  const SAFE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
49
- async function initRepo({ repoRoot = process.cwd(), homeDir = homedir(), mode = "link", selected, force = false, dryRun = false } = {}) {
218
+ async function initRepo(options = {}) {
219
+ const { repoRoot = process.cwd(), homeDir = homedir(), mode = "link", selected, force = false, dryRun = false } = options;
50
220
  if (!selected) throw new Error("selected assets are required");
221
+ const scope = options.scope ?? "project";
222
+ const ruleWiring = options.ruleWiring ?? "reference";
223
+ validateScope(scope);
224
+ validateRuleWiring(ruleWiring);
225
+ const tools = normalizeTools(options.tools);
51
226
  validateMode(mode);
52
227
  const paths = cachePaths({ homeDir });
53
228
  const registry = await readCachedRegistry(paths.assetsRoot);
@@ -55,6 +230,17 @@ async function initRepo({ repoRoot = process.cwd(), homeDir = homedir(), mode =
55
230
  const assets = normalizeSelected(selected);
56
231
  const agentsRoot = join(repoRoot, ".agents");
57
232
  validateSelectedAssets(registry, assets);
233
+ if (scope === "global") return await initGlobal({
234
+ homeDir,
235
+ repoRoot,
236
+ paths,
237
+ registry,
238
+ cacheManifest,
239
+ assets,
240
+ tools,
241
+ ruleWiring,
242
+ dryRun
243
+ });
58
244
  const plan = await buildInitPlan({
59
245
  repoRoot,
60
246
  agentsRoot,
@@ -75,21 +261,62 @@ async function initRepo({ repoRoot = process.cwd(), homeDir = homedir(), mode =
75
261
  cacheRoot: paths.assetsRoot,
76
262
  assets,
77
263
  assetSnapshot: snapshotSelectedAssetMetadata(registry, assets),
264
+ scope,
265
+ tools,
266
+ ruleWiring,
78
267
  initializedAt: (/* @__PURE__ */ new Date()).toISOString()
79
268
  };
269
+ const rulePlan = await planRuleInstall({
270
+ repoRoot,
271
+ homeDir,
272
+ cacheRoot: paths.assetsRoot,
273
+ scope: "project",
274
+ tools,
275
+ ruleWiring,
276
+ selectedRules: assets.rules,
277
+ rulePaths: projectRulePathMap(repoRoot, paths.assetsRoot, registry, assets, mode, ruleWiring)
278
+ });
80
279
  if (dryRun) return {
280
+ ...manifest,
81
281
  dryRun: true,
82
- mode,
83
- source: cacheManifest.source,
84
- cacheRoot: paths.assetsRoot,
85
- assets,
86
- assetSnapshot: snapshotSelectedAssetMetadata(registry, assets),
87
- files: plan.files
282
+ files: uniqueFiles([...plan.files, ...rulePlan.files])
88
283
  };
89
284
  await mkdir(agentsRoot, { recursive: true });
90
285
  if (mode !== "pointer") await installAssets(plan.assets);
91
286
  await writeJson(join(agentsRoot, "manifest.json"), manifest);
92
287
  await upsertAgentsSection(repoRoot);
288
+ await applyRuleInstall(rulePlan);
289
+ return manifest;
290
+ }
291
+ async function initGlobal({ homeDir, repoRoot, paths, registry, cacheManifest, assets, tools, ruleWiring, dryRun }) {
292
+ if (assets.skills.length > 0) throw new Error("Global installs currently support rules only");
293
+ const rulePlan = await planRuleInstall({
294
+ repoRoot,
295
+ homeDir,
296
+ cacheRoot: paths.assetsRoot,
297
+ scope: "global",
298
+ tools,
299
+ ruleWiring,
300
+ selectedRules: assets.rules,
301
+ rulePaths: rulePathMap(paths.assetsRoot, registry, assets.rules)
302
+ });
303
+ const manifest = {
304
+ scope: "global",
305
+ source: cacheManifest.source,
306
+ cacheRoot: paths.assetsRoot,
307
+ assets,
308
+ assetSnapshot: snapshotSelectedAssetMetadata(registry, assets),
309
+ tools,
310
+ ruleWiring,
311
+ initializedAt: (/* @__PURE__ */ new Date()).toISOString()
312
+ };
313
+ if (dryRun) return {
314
+ ...manifest,
315
+ dryRun: true,
316
+ files: uniqueFiles([...rulePlan.files, join(homeDir, ".deweyou/agents/global-manifest.json")])
317
+ };
318
+ await applyRuleInstall(rulePlan);
319
+ await writeJson(join(homeDir, ".deweyou/agents/global-manifest.json"), manifest);
93
320
  return manifest;
94
321
  }
95
322
  async function runInit(flags = {}, { promptForInit } = {}) {
@@ -98,6 +325,9 @@ async function runInit(flags = {}, { promptForInit } = {}) {
98
325
  const registry = await readCachedRegistry(cachePaths({ homeDir }).assetsRoot);
99
326
  const scripted = hasScriptedSelectionFlags(flags);
100
327
  let mode = flags.mode ?? "link";
328
+ let scope = flags.scope;
329
+ let tools = flags.tools;
330
+ let ruleWiring = flags.ruleWiring;
101
331
  let selected;
102
332
  validateMode(mode);
103
333
  if (flags.yes && !scripted) throw new Error("--yes requires --all, --skills, or --rules");
@@ -109,15 +339,25 @@ async function runInit(flags = {}, { promptForInit } = {}) {
109
339
  const prompted = await (promptForInit ?? await loadPromptForInit())({
110
340
  registry,
111
341
  repoRoot,
112
- mode: flags.mode
342
+ mode: flags.mode,
343
+ scope: flags.scope,
344
+ tools: flags.tools,
345
+ ruleWiring: flags.ruleWiring
113
346
  });
114
- mode = prompted.mode;
347
+ mode = flags.mode ?? prompted.mode;
348
+ scope = flags.scope ?? prompted.scope;
349
+ tools = flags.tools ?? prompted.tools;
350
+ ruleWiring = flags.ruleWiring ?? prompted.ruleWiring;
115
351
  selected = prompted.selected;
116
352
  }
117
353
  if (!hasSelectedAssets(selected)) throw new Error("No assets selected. Pass --all, --skills, or --rules, or run interactive setup.");
354
+ if (scripted && scope === "global" && !flags.yes && !flags.dryRun) throw new Error("--scope global with scripted selections requires --yes or --dry-run");
118
355
  const manifest = await initRepo({
119
356
  repoRoot,
120
357
  mode,
358
+ scope,
359
+ tools,
360
+ ruleWiring,
121
361
  selected,
122
362
  force: flags.force ?? false,
123
363
  dryRun: flags.dryRun ?? false,
@@ -130,7 +370,7 @@ async function runInit(flags = {}, { promptForInit } = {}) {
130
370
  return manifest;
131
371
  }
132
372
  async function loadPromptForInit() {
133
- const { promptForInit } = await import("./prompts-DVRcV560.mjs");
373
+ const { promptForInit } = await import("./prompts-DdaeRkBa.mjs");
134
374
  return promptForInit;
135
375
  }
136
376
  async function readCachedRegistry(assetsRoot) {
@@ -175,6 +415,22 @@ function validateMode(mode) {
175
415
  if (typeof mode !== "string") throw new Error("mode must be one of link, copy, or pointer");
176
416
  if (!VALID_MODES.has(mode)) throw new Error("mode must be one of link, copy, or pointer");
177
417
  }
418
+ function validateScope(scope) {
419
+ if (typeof scope !== "string") throw new Error("scope must be one of project or global");
420
+ if (!VALID_SCOPES.has(scope)) throw new Error("scope must be one of project or global");
421
+ }
422
+ function normalizeTools(tools) {
423
+ if (tools) {
424
+ for (const tool of tools) if (typeof tool !== "string" || !VALID_TOOL_SELECTIONS.has(tool)) throw new Error(`tool must be one of codex or claude: ${tool}`);
425
+ }
426
+ const selected = !tools || tools.includes("all") ? ["codex", "claude"] : [...tools];
427
+ for (const tool of selected) if (typeof tool !== "string" || !VALID_TOOLS.has(tool)) throw new Error(`tool must be one of codex or claude: ${tool}`);
428
+ return [...new Set(selected)];
429
+ }
430
+ function validateRuleWiring(ruleWiring) {
431
+ if (typeof ruleWiring !== "string") throw new Error("ruleWiring must be one of reference or inline");
432
+ if (!VALID_RULE_WIRING.has(ruleWiring)) throw new Error("ruleWiring must be one of reference or inline");
433
+ }
178
434
  function validateSelectedAssets(registry, selected) {
179
435
  for (const skill of selected.skills) {
180
436
  if (!registry.assets.skills[skill]) throw new Error(`Unknown Dewey skill: ${skill}`);
@@ -206,6 +462,16 @@ function pickAssetMetadata(asset) {
206
462
  hash: asset.hash
207
463
  };
208
464
  }
465
+ function uniqueFiles(files) {
466
+ return [...new Set(files)];
467
+ }
468
+ function rulePathMap(assetsRoot, registry, rules) {
469
+ return new Map(rules.map((rule) => [rule, join(assetsRoot, registry.assets.rules[rule].path)]));
470
+ }
471
+ function projectRulePathMap(repoRoot, assetsRoot, registry, assets, mode, ruleWiring) {
472
+ if (mode === "pointer" || ruleWiring === "inline") return rulePathMap(assetsRoot, registry, assets.rules);
473
+ return new Map(assets.rules.map((rule) => [rule, join(repoRoot, ".agents", "rules", `${rule}.md`)]));
474
+ }
209
475
  async function buildInitPlan({ repoRoot, agentsRoot, assetsRoot, registry, assets, mode }) {
210
476
  const assetPlans = mode === "pointer" ? [] : await Promise.all([...assets.skills.map((id) => buildAssetPlan({
211
477
  kind: "skill",
@@ -0,0 +1,189 @@
1
+ import { cancel, confirm, intro, isCancel, multiselect, note, select } from "@clack/prompts";
2
+ //#region src/cli/prompts.ts
3
+ const SETUP_SCOPES = [{
4
+ value: "project",
5
+ label: "project",
6
+ hint: "Install into this repository."
7
+ }, {
8
+ value: "global",
9
+ label: "global",
10
+ hint: "Install into Codex and Claude user homes."
11
+ }];
12
+ const TOOL_OPTIONS = [
13
+ {
14
+ value: "both",
15
+ label: "both",
16
+ hint: "Wire Codex and Claude Code."
17
+ },
18
+ {
19
+ value: "codex",
20
+ label: "codex",
21
+ hint: "Wire AGENTS.md only."
22
+ },
23
+ {
24
+ value: "claude",
25
+ label: "claude",
26
+ hint: "Wire CLAUDE.md only."
27
+ }
28
+ ];
29
+ const SETUP_MODES = [
30
+ {
31
+ value: "link",
32
+ label: "link",
33
+ hint: "Symlink assets from the Dewey cache."
34
+ },
35
+ {
36
+ value: "copy",
37
+ label: "copy",
38
+ hint: "Copy asset files into this repository."
39
+ },
40
+ {
41
+ value: "pointer",
42
+ label: "pointer",
43
+ hint: "Write only the manifest and AGENTS.md pointers."
44
+ }
45
+ ];
46
+ const RULE_WIRING_OPTIONS = [{
47
+ value: "reference",
48
+ label: "reference",
49
+ hint: "Reference selected rule files."
50
+ }, {
51
+ value: "inline",
52
+ label: "inline",
53
+ hint: "Inline selected rule bodies."
54
+ }];
55
+ const ASSET_SCOPES = [
56
+ {
57
+ value: "all",
58
+ label: "all",
59
+ hint: "Enable every cached skill and rule."
60
+ },
61
+ {
62
+ value: "custom",
63
+ label: "custom",
64
+ hint: "Choose skills and rules individually."
65
+ },
66
+ {
67
+ value: "skills",
68
+ label: "skills only",
69
+ hint: "Choose skills without installing rules."
70
+ },
71
+ {
72
+ value: "rules",
73
+ label: "rules only",
74
+ hint: "Choose rules without installing skills."
75
+ }
76
+ ];
77
+ const GLOBAL_ASSET_SCOPES = [{
78
+ value: "all",
79
+ label: "all rules",
80
+ hint: "Enable every cached rule."
81
+ }, {
82
+ value: "rules",
83
+ label: "choose rules",
84
+ hint: "Choose rules individually."
85
+ }];
86
+ async function promptForInit({ registry, repoRoot, mode, scope, tools, ruleWiring }) {
87
+ intro("Dewey Agent Setup");
88
+ note(repoRoot, "Repository");
89
+ const selectedScope = scope ?? await promptOrExit(select({
90
+ message: "Select install scope",
91
+ options: SETUP_SCOPES
92
+ }));
93
+ const selectedTools = tools === void 0 ? normalizePromptTools(await promptOrExit(select({
94
+ message: "Select tools",
95
+ options: TOOL_OPTIONS
96
+ }))) : normalizeToolSelection(tools);
97
+ const selectedMode = selectedScope === "global" ? "pointer" : mode ?? await promptOrExit(select({
98
+ message: "Select setup mode",
99
+ options: SETUP_MODES
100
+ }));
101
+ const selected = await selectAssets({
102
+ registry,
103
+ scope: await promptOrExit(select({
104
+ message: "Select asset scope",
105
+ options: selectedScope === "global" ? GLOBAL_ASSET_SCOPES : ASSET_SCOPES
106
+ })),
107
+ installScope: selectedScope
108
+ });
109
+ const selectedRuleWiring = ruleWiring ?? (selected.rules.length > 0 ? await promptOrExit(select({
110
+ message: "Select rule wiring",
111
+ options: RULE_WIRING_OPTIONS
112
+ })) : "reference");
113
+ note(plannedFiles({
114
+ repoRoot,
115
+ scope: selectedScope,
116
+ tools: selectedTools,
117
+ selected
118
+ }), "Dewey will update");
119
+ if (!await promptOrExit(confirm({ message: `Enable ${selected.skills.length} skill(s) and ${selected.rules.length} rule(s) using ${selectedMode} mode?` }))) exitCancelled();
120
+ return {
121
+ mode: selectedMode,
122
+ scope: selectedScope,
123
+ tools: selectedTools,
124
+ ruleWiring: selectedRuleWiring,
125
+ selected
126
+ };
127
+ }
128
+ async function selectAssets({ registry, scope, installScope }) {
129
+ if (scope === "all") return {
130
+ skills: installScope === "global" ? [] : Object.keys(registry.assets.skills),
131
+ rules: Object.keys(registry.assets.rules)
132
+ };
133
+ const selected = {
134
+ skills: [],
135
+ rules: []
136
+ };
137
+ if (installScope === "project" && (scope === "custom" || scope === "skills")) selected.skills = await promptOrExit(multiselect({
138
+ message: "Select skills",
139
+ options: assetOptions(registry.assets.skills),
140
+ required: false
141
+ }));
142
+ if (scope === "custom" || scope === "rules") selected.rules = await promptOrExit(multiselect({
143
+ message: "Select rules",
144
+ options: assetOptions(registry.assets.rules),
145
+ required: false
146
+ }));
147
+ return selected;
148
+ }
149
+ function assetOptions(assets) {
150
+ return Object.entries(assets).map(([name, asset]) => ({
151
+ value: name,
152
+ label: name,
153
+ hint: asset.description
154
+ }));
155
+ }
156
+ function normalizePromptTools(selectedTools) {
157
+ if (selectedTools === "both") return ["codex", "claude"];
158
+ return [selectedTools];
159
+ }
160
+ function normalizeToolSelection(tools) {
161
+ if (tools.includes("all")) return ["codex", "claude"];
162
+ return [...new Set(tools)];
163
+ }
164
+ function plannedFiles({ repoRoot, scope, tools, selected }) {
165
+ const files = [];
166
+ if (scope === "global") {
167
+ if (tools.includes("codex")) files.push("~/.codex/AGENTS.md");
168
+ if (tools.includes("claude")) files.push("~/.claude/CLAUDE.md");
169
+ files.push("~/.deweyou/agents/global-manifest.json");
170
+ return files.join("\n");
171
+ }
172
+ files.push("AGENTS.md");
173
+ if (tools.includes("claude") && selected.rules.length > 0) files.push("CLAUDE.md");
174
+ files.push(".agents/manifest.json");
175
+ if (selected.skills.length > 0) files.push(".agents/skills/<skill>/SKILL.md");
176
+ if (selected.rules.length > 0) files.push(".agents/rules/<rule>.md");
177
+ return `${repoRoot}\n\n${files.join("\n")}`;
178
+ }
179
+ async function promptOrExit(prompt) {
180
+ const value = await prompt;
181
+ if (isCancel(value)) exitCancelled();
182
+ return value;
183
+ }
184
+ function exitCancelled() {
185
+ cancel("Dewey agent setup cancelled.");
186
+ process.exit(0);
187
+ }
188
+ //#endregion
189
+ export { promptForInit };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deweyou-cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Dewey's personal agent workflow bootstrapper",
6
6
  "files": [
@@ -1,100 +0,0 @@
1
- import { cancel, confirm, intro, isCancel, multiselect, note, select } from "@clack/prompts";
2
- //#region src/cli/prompts.ts
3
- const SETUP_MODES = [
4
- {
5
- value: "link",
6
- label: "link",
7
- hint: "Symlink assets from the Dewey cache."
8
- },
9
- {
10
- value: "copy",
11
- label: "copy",
12
- hint: "Copy asset files into this repository."
13
- },
14
- {
15
- value: "pointer",
16
- label: "pointer",
17
- hint: "Write only the manifest and AGENTS.md pointers."
18
- }
19
- ];
20
- const ASSET_SCOPES = [
21
- {
22
- value: "all",
23
- label: "all",
24
- hint: "Enable every cached skill and rule."
25
- },
26
- {
27
- value: "custom",
28
- label: "custom",
29
- hint: "Choose skills and rules individually."
30
- },
31
- {
32
- value: "skills",
33
- label: "skills only",
34
- hint: "Choose skills without installing rules."
35
- },
36
- {
37
- value: "rules",
38
- label: "rules only",
39
- hint: "Choose rules without installing skills."
40
- }
41
- ];
42
- async function promptForInit({ registry, repoRoot, mode }) {
43
- intro("Dewey Agent Setup");
44
- note(repoRoot, "Repository");
45
- const selectedMode = mode ?? await promptOrExit(select({
46
- message: "Select setup mode",
47
- options: SETUP_MODES
48
- }));
49
- const selected = await selectAssets({
50
- registry,
51
- scope: await promptOrExit(select({
52
- message: "Select asset scope",
53
- options: ASSET_SCOPES
54
- }))
55
- });
56
- if (!await promptOrExit(confirm({ message: `Enable ${selected.skills.length} skill(s) and ${selected.rules.length} rule(s) using ${selectedMode} mode?` }))) exitCancelled();
57
- return {
58
- mode: selectedMode,
59
- selected
60
- };
61
- }
62
- async function selectAssets({ registry, scope }) {
63
- if (scope === "all") return {
64
- skills: Object.keys(registry.assets.skills),
65
- rules: Object.keys(registry.assets.rules)
66
- };
67
- const selected = {
68
- skills: [],
69
- rules: []
70
- };
71
- if (scope === "custom" || scope === "skills") selected.skills = await promptOrExit(multiselect({
72
- message: "Select skills",
73
- options: assetOptions(registry.assets.skills),
74
- required: false
75
- }));
76
- if (scope === "custom" || scope === "rules") selected.rules = await promptOrExit(multiselect({
77
- message: "Select rules",
78
- options: assetOptions(registry.assets.rules),
79
- required: false
80
- }));
81
- return selected;
82
- }
83
- function assetOptions(assets) {
84
- return Object.entries(assets).map(([name, asset]) => ({
85
- value: name,
86
- label: name,
87
- hint: asset.description
88
- }));
89
- }
90
- async function promptOrExit(prompt) {
91
- const value = await prompt;
92
- if (isCancel(value)) exitCancelled();
93
- return value;
94
- }
95
- function exitCancelled() {
96
- cancel("Dewey agent setup cancelled.");
97
- process.exit(0);
98
- }
99
- //#endregion
100
- export { promptForInit };