coding-friend-cli 1.9.1 → 1.10.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/README.md CHANGED
@@ -29,6 +29,8 @@ cf host [path] # Build and serve learning docs at localhost:3333
29
29
  cf mcp [path] # Setup MCP server for LLM integration
30
30
  # [path] is optional, default is `docs/learn`
31
31
  # This prints a JSON config snippet to add to your client's MCP
32
+ cf permission # Manage Claude Code permission rules for Coding Friend
33
+ cf permission --all # Apply all recommended permissions without prompts
32
34
  cf statusline # Setup coding-friend statusline
33
35
  cf update # Update plugin + CLI + statusline
34
36
  cf update --cli # Update only the CLI (npm package)
@@ -0,0 +1,198 @@
1
+ import {
2
+ readJson,
3
+ writeJson
4
+ } from "./chunk-HQ5BPWEV.js";
5
+
6
+ // src/lib/permissions.ts
7
+ var PERMISSION_RULES = [
8
+ // Core (hooks & infrastructure)
9
+ {
10
+ rule: "Bash(cat:*)",
11
+ description: "[read-only] Read file contents (\u26A0 system-wide scope, project boundary enforced by Claude Code) \xB7 Used by: session-init hook",
12
+ category: "Core",
13
+ recommended: true
14
+ },
15
+ {
16
+ rule: "Bash(grep:*)",
17
+ description: "[read-only] Search file contents (\u26A0 system-wide scope, project boundary enforced by Claude Code) \xB7 Used by: session-init hook, skills",
18
+ category: "Core",
19
+ recommended: true
20
+ },
21
+ {
22
+ rule: "Bash(sed:*)",
23
+ description: "[modify] Text transformation \xB7 Used by: session-init hook",
24
+ category: "Core",
25
+ recommended: true
26
+ },
27
+ {
28
+ rule: "Bash(tr:*)",
29
+ description: "[read-only] Character translation \xB7 Used by: session-init hook",
30
+ category: "Core",
31
+ recommended: true
32
+ },
33
+ {
34
+ rule: "Bash(wc:*)",
35
+ description: "[read-only] Count lines/words \xB7 Used by: cf-verification, skills",
36
+ category: "Core",
37
+ recommended: true
38
+ },
39
+ {
40
+ rule: "Bash(mkdir:*)",
41
+ description: "[write] Create directories \xB7 Used by: docs folder setup",
42
+ category: "Core",
43
+ recommended: true
44
+ },
45
+ // Git Operations
46
+ {
47
+ rule: "Bash(git add:*)",
48
+ description: "[modify] Stage files for commit \xB7 Used by: /cf-commit, /cf-ship",
49
+ category: "Git",
50
+ recommended: true
51
+ },
52
+ {
53
+ rule: "Bash(git commit:*)",
54
+ description: "[modify] Create commits \xB7 Used by: /cf-commit, /cf-ship",
55
+ category: "Git",
56
+ recommended: true
57
+ },
58
+ {
59
+ rule: "Bash(git status:*)",
60
+ description: "[read-only] Check working tree status \xB7 Used by: /cf-commit, /cf-review, cf-verification",
61
+ category: "Git",
62
+ recommended: true
63
+ },
64
+ {
65
+ rule: "Bash(git diff:*)",
66
+ description: "[read-only] View file changes \xB7 Used by: /cf-commit, /cf-review, cf-verification",
67
+ category: "Git",
68
+ recommended: true
69
+ },
70
+ {
71
+ rule: "Bash(git log:*)",
72
+ description: "[read-only] View commit history \xB7 Used by: /cf-commit, /cf-review, cf-sys-debug",
73
+ category: "Git",
74
+ recommended: true
75
+ },
76
+ {
77
+ rule: "Bash(git push:*)",
78
+ description: "[remote] Push commits to remote \xB7 Used by: /cf-ship",
79
+ category: "Git",
80
+ recommended: true
81
+ },
82
+ {
83
+ rule: "Bash(git pull:*)",
84
+ description: "[remote] Pull changes from remote \xB7 Used by: /cf-ship",
85
+ category: "Git",
86
+ recommended: true
87
+ },
88
+ {
89
+ rule: "Bash(gh pr create:*)",
90
+ description: "[remote] Create GitHub pull requests \xB7 Used by: /cf-ship",
91
+ category: "Git",
92
+ recommended: true
93
+ },
94
+ // Testing & Build
95
+ {
96
+ rule: "Bash(npm test:*)",
97
+ description: "[execute] Run test suites \xB7 Used by: cf-verification, /cf-fix, cf-tdd",
98
+ category: "Testing & Build",
99
+ recommended: true
100
+ },
101
+ {
102
+ rule: "Bash(npm run:*)",
103
+ description: "[execute] Run npm scripts (build, lint, format) \xB7 Used by: cf-verification",
104
+ category: "Testing & Build",
105
+ recommended: true
106
+ },
107
+ // Web & Research
108
+ {
109
+ rule: "WebSearch",
110
+ description: "[network] Perform web searches \xB7 Used by: /cf-research",
111
+ category: "Web & Research",
112
+ recommended: false
113
+ },
114
+ {
115
+ rule: "WebFetch(domain:*)",
116
+ description: "[network] Fetch content from web pages \xB7 Used by: /cf-research",
117
+ category: "Web & Research",
118
+ recommended: false
119
+ }
120
+ ];
121
+ function getExistingRules(settingsPath) {
122
+ const settings = readJson(settingsPath);
123
+ if (!settings) return [];
124
+ const permissions = settings.permissions;
125
+ return permissions?.allow ?? [];
126
+ }
127
+ function getMissingRules(existing, rules) {
128
+ return rules.filter((r) => !existing.includes(r.rule));
129
+ }
130
+ function buildLearnDirRules(learnPath, autoCommit) {
131
+ const rules = [
132
+ {
133
+ rule: `Read(${learnPath}/**)`,
134
+ description: "[read-only] Read learning docs \xB7 Used by: /cf-learn",
135
+ category: "External Learn Directory",
136
+ recommended: true
137
+ },
138
+ {
139
+ rule: `Edit(${learnPath}/**)`,
140
+ description: "[modify] Edit learning docs \xB7 Used by: /cf-learn",
141
+ category: "External Learn Directory",
142
+ recommended: true
143
+ },
144
+ {
145
+ rule: `Write(${learnPath}/**)`,
146
+ description: "[write] Write learning docs \xB7 Used by: /cf-learn",
147
+ category: "External Learn Directory",
148
+ recommended: true
149
+ }
150
+ ];
151
+ if (autoCommit) {
152
+ const quoted = learnPath.includes(" ") ? `"${learnPath}"` : learnPath;
153
+ rules.push({
154
+ rule: `Bash(cd ${quoted} && git add:*)`,
155
+ description: "[modify] Stage learning docs for commit \xB7 Used by: /cf-learn auto-commit",
156
+ category: "External Learn Directory",
157
+ recommended: true
158
+ });
159
+ rules.push({
160
+ rule: `Bash(cd ${quoted} && git commit:*)`,
161
+ description: "[modify] Commit learning docs \xB7 Used by: /cf-learn auto-commit",
162
+ category: "External Learn Directory",
163
+ recommended: true
164
+ });
165
+ }
166
+ return rules;
167
+ }
168
+ function applyPermissions(settingsPath, toAdd, toRemove) {
169
+ const settings = readJson(settingsPath) ?? {};
170
+ const permissions = settings.permissions ?? {};
171
+ const existing = permissions.allow ?? [];
172
+ const afterRemove = existing.filter((r) => !toRemove.includes(r));
173
+ const afterAdd = [
174
+ ...afterRemove,
175
+ ...toAdd.filter((r) => !afterRemove.includes(r))
176
+ ];
177
+ permissions.allow = afterAdd;
178
+ settings.permissions = permissions;
179
+ writeJson(settingsPath, settings);
180
+ }
181
+ function groupByCategory(rules) {
182
+ const groups = /* @__PURE__ */ new Map();
183
+ for (const rule of rules) {
184
+ const list = groups.get(rule.category) ?? [];
185
+ list.push(rule);
186
+ groups.set(rule.category, list);
187
+ }
188
+ return groups;
189
+ }
190
+
191
+ export {
192
+ PERMISSION_RULES,
193
+ getExistingRules,
194
+ getMissingRules,
195
+ buildLearnDirRules,
196
+ applyPermissions,
197
+ groupByCategory
198
+ };
@@ -5,7 +5,7 @@ import {
5
5
  globalConfigPath,
6
6
  localConfigPath,
7
7
  readJson
8
- } from "./chunk-TPRZHSFS.js";
8
+ } from "./chunk-HQ5BPWEV.js";
9
9
  import {
10
10
  log
11
11
  } from "./chunk-W5CD7WTX.js";
@@ -26,8 +26,8 @@ async function askScope(label = "Save to:") {
26
26
  return select({
27
27
  message: label,
28
28
  choices: [
29
- { name: "Global (all projects)", value: "global" },
30
29
  { name: "This project only", value: "local" },
30
+ { name: "Global (all projects)", value: "global" },
31
31
  new Separator(),
32
32
  { name: "Back", value: "back" }
33
33
  ]
@@ -21,7 +21,7 @@ ${MARKER_START}
21
21
  _cf_completions() {
22
22
  local cur="\${COMP_WORDS[COMP_CWORD]}"
23
23
  local prev="\${COMP_WORDS[COMP_CWORD-1]}"
24
- local commands="install uninstall init config host mcp statusline update dev session"
24
+ local commands="install uninstall init config host mcp permission statusline update dev session"
25
25
 
26
26
  # Subcommands for 'dev'
27
27
  if [[ "\${COMP_WORDS[1]}" == "dev" && \${COMP_CWORD} -eq 2 ]]; then
@@ -55,6 +55,7 @@ var ZSH_FUNCTION_BODY = `_cf() {
55
55
  'config:Manage Coding Friend configuration'
56
56
  'host:Build and serve learning docs as a static website'
57
57
  'mcp:Setup MCP server for learning docs'
58
+ 'permission:Manage Claude Code permission rules for Coding Friend'
58
59
  'statusline:Setup coding-friend statusline in Claude Code'
59
60
  'update:Update coding-friend plugin and refresh statusline'
60
61
  'dev:Switch between local and remote plugin for development'
@@ -103,6 +104,7 @@ complete -c cf -n "__fish_use_subcommand" -a init -d "Initialize coding-friend i
103
104
  complete -c cf -n "__fish_use_subcommand" -a config -d "Manage Coding Friend configuration"
104
105
  complete -c cf -n "__fish_use_subcommand" -a host -d "Build and serve learning docs as a static website"
105
106
  complete -c cf -n "__fish_use_subcommand" -a mcp -d "Setup MCP server for learning docs"
107
+ complete -c cf -n "__fish_use_subcommand" -a permission -d "Manage Claude Code permission rules"
106
108
  complete -c cf -n "__fish_use_subcommand" -a statusline -d "Setup coding-friend statusline in Claude Code"
107
109
  complete -c cf -n "__fish_use_subcommand" -a update -d "Update coding-friend plugin and refresh statusline"
108
110
  complete -c cf -n "__fish_use_subcommand" -a dev -d "Switch between local and remote plugin for development"
@@ -121,7 +123,7 @@ var POWERSHELL_BLOCK = `
121
123
  ${MARKER_START}
122
124
  Register-ArgumentCompleter -Native -CommandName cf -ScriptBlock {
123
125
  param($wordToComplete, $commandAst, $cursorPosition)
124
- $commands = @('install','uninstall','init','config','host','mcp','statusline','update','dev','session')
126
+ $commands = @('install','uninstall','init','config','host','mcp','permission','statusline','update','dev','session')
125
127
  $devSubcommands = @('on','off','status','restart','sync','update')
126
128
  $sessionSubcommands = @('save','load')
127
129
  $words = $commandAst.CommandElements
@@ -6,7 +6,7 @@ import {
6
6
  localConfigPath,
7
7
  readJson,
8
8
  resolvePath
9
- } from "./chunk-TPRZHSFS.js";
9
+ } from "./chunk-HQ5BPWEV.js";
10
10
 
11
11
  // src/lib/config.ts
12
12
  function loadConfig() {
@@ -38,6 +38,9 @@ function globalConfigPath() {
38
38
  function claudeSettingsPath() {
39
39
  return join(homedir(), ".claude", "settings.json");
40
40
  }
41
+ function claudeLocalSettingsPath() {
42
+ return resolve(process.cwd(), ".claude", "settings.local.json");
43
+ }
41
44
  function installedPluginsPath() {
42
45
  return join(homedir(), ".claude", "plugins", "installed_plugins.json");
43
46
  }
@@ -96,6 +99,7 @@ export {
96
99
  localConfigPath,
97
100
  globalConfigPath,
98
101
  claudeSettingsPath,
102
+ claudeLocalSettingsPath,
99
103
  installedPluginsPath,
100
104
  pluginCachePath,
101
105
  devStatePath,
@@ -2,7 +2,7 @@ import {
2
2
  installedPluginsPath,
3
3
  knownMarketplacesPath,
4
4
  readJson
5
- } from "./chunk-TPRZHSFS.js";
5
+ } from "./chunk-HQ5BPWEV.js";
6
6
 
7
7
  // src/lib/plugin-state.ts
8
8
  var MARKETPLACE_NAME = "coding-friend-marketplace";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  ensureStatusline,
3
3
  getInstalledVersion
4
- } from "./chunk-BPLN4LDL.js";
4
+ } from "./chunk-YAOUAJZP.js";
5
5
  import {
6
6
  ensureShellCompletion
7
- } from "./chunk-7N64TDZ6.js";
7
+ } from "./chunk-BCYBFGVD.js";
8
8
  import {
9
9
  commandExists,
10
10
  run,
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  claudeSettingsPath,
15
15
  readJson
16
- } from "./chunk-TPRZHSFS.js";
16
+ } from "./chunk-HQ5BPWEV.js";
17
17
  import {
18
18
  log
19
19
  } from "./chunk-W5CD7WTX.js";
@@ -10,7 +10,7 @@ import {
10
10
  pluginCachePath,
11
11
  readJson,
12
12
  writeJson
13
- } from "./chunk-TPRZHSFS.js";
13
+ } from "./chunk-HQ5BPWEV.js";
14
14
 
15
15
  // src/lib/statusline.ts
16
16
  import { existsSync, readdirSync } from "fs";
@@ -7,19 +7,19 @@ import {
7
7
  getScopeLabel,
8
8
  injectBackChoice,
9
9
  showConfigHint
10
- } from "./chunk-QQ5SVZET.js";
10
+ } from "./chunk-AHGBESGW.js";
11
11
  import {
12
12
  findStatuslineHookPath,
13
13
  isStatuslineConfigured,
14
14
  saveStatuslineConfig,
15
15
  selectStatuslineComponents,
16
16
  writeStatuslineSettings
17
- } from "./chunk-BPLN4LDL.js";
17
+ } from "./chunk-YAOUAJZP.js";
18
18
  import {
19
19
  ensureShellCompletion,
20
20
  hasShellCompletion,
21
21
  removeShellCompletion
22
- } from "./chunk-7N64TDZ6.js";
22
+ } from "./chunk-BCYBFGVD.js";
23
23
  import {
24
24
  ALL_COMPONENT_IDS,
25
25
  DEFAULT_CONFIG
@@ -33,7 +33,7 @@ import {
33
33
  mergeJson,
34
34
  readJson,
35
35
  resolvePath
36
- } from "./chunk-TPRZHSFS.js";
36
+ } from "./chunk-HQ5BPWEV.js";
37
37
  import {
38
38
  log
39
39
  } from "./chunk-W5CD7WTX.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  isMarketplaceRegistered,
3
3
  isPluginInstalled
4
- } from "./chunk-HFLBFX6J.js";
4
+ } from "./chunk-TLLWOWYU.js";
5
5
  import {
6
6
  ensureStatusline
7
- } from "./chunk-BPLN4LDL.js";
7
+ } from "./chunk-YAOUAJZP.js";
8
8
  import {
9
9
  ensureShellCompletion
10
- } from "./chunk-7N64TDZ6.js";
10
+ } from "./chunk-BCYBFGVD.js";
11
11
  import "./chunk-PGLUEN7D.js";
12
12
  import {
13
13
  commandExists,
@@ -19,7 +19,7 @@ import {
19
19
  pluginCachePath,
20
20
  readJson,
21
21
  writeJson
22
- } from "./chunk-TPRZHSFS.js";
22
+ } from "./chunk-HQ5BPWEV.js";
23
23
  import {
24
24
  log
25
25
  } from "./chunk-W5CD7WTX.js";
@@ -3,13 +3,13 @@ import {
3
3
  } from "./chunk-RZRT7NGT.js";
4
4
  import {
5
5
  resolveDocsDir
6
- } from "./chunk-FYHHNX7K.js";
6
+ } from "./chunk-EDVZPZ3B.js";
7
7
  import "./chunk-PGLUEN7D.js";
8
8
  import {
9
9
  run,
10
10
  streamExec
11
11
  } from "./chunk-UFGNO6CW.js";
12
- import "./chunk-TPRZHSFS.js";
12
+ import "./chunk-HQ5BPWEV.js";
13
13
  import {
14
14
  log
15
15
  } from "./chunk-W5CD7WTX.js";
package/dist/index.js CHANGED
@@ -14,35 +14,39 @@ program.name("cf").description(
14
14
  "coding-friend CLI \u2014 host learning docs, setup MCP, init projects"
15
15
  ).version(pkg.version, "-v, --version");
16
16
  program.command("install").description("Install the Coding Friend plugin into Claude Code").action(async () => {
17
- const { installCommand } = await import("./install-D4NW3OAA.js");
17
+ const { installCommand } = await import("./install-YEAWW7ZX.js");
18
18
  await installCommand();
19
19
  });
20
20
  program.command("uninstall").description("Uninstall the Coding Friend plugin from Claude Code").action(async () => {
21
- const { uninstallCommand } = await import("./uninstall-SOHU5WGK.js");
21
+ const { uninstallCommand } = await import("./uninstall-3ISPGQHE.js");
22
22
  await uninstallCommand();
23
23
  });
24
24
  program.command("init").description("Initialize coding-friend in current project").action(async () => {
25
- const { initCommand } = await import("./init-CIEDOFNC.js");
25
+ const { initCommand } = await import("./init-BG2ESQQC.js");
26
26
  await initCommand();
27
27
  });
28
28
  program.command("config").description("Manage Coding Friend configuration").action(async () => {
29
- const { configCommand } = await import("./config-VAML7F7K.js");
29
+ const { configCommand } = await import("./config-6P3SE3NS.js");
30
30
  await configCommand();
31
31
  });
32
32
  program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
33
- const { hostCommand } = await import("./host-LOG5RPZ7.js");
33
+ const { hostCommand } = await import("./host-BPO2STZA.js");
34
34
  await hostCommand(path, opts);
35
35
  });
36
36
  program.command("mcp").description("Setup MCP server for learning docs").argument("[path]", "path to docs folder").action(async (path) => {
37
- const { mcpCommand } = await import("./mcp-ORMYETXQ.js");
37
+ const { mcpCommand } = await import("./mcp-54UB5MF2.js");
38
38
  await mcpCommand(path);
39
39
  });
40
+ program.command("permission").description("Manage Claude Code permission rules for Coding Friend").option("--all", "Apply all recommended permissions without prompts").action(async (opts) => {
41
+ const { permissionCommand } = await import("./permission-GPOUJUZS.js");
42
+ await permissionCommand(opts);
43
+ });
40
44
  program.command("statusline").description("Setup coding-friend statusline in Claude Code").action(async () => {
41
- const { statuslineCommand } = await import("./statusline-5HWRTSVL.js");
45
+ const { statuslineCommand } = await import("./statusline-L2CYEV4H.js");
42
46
  await statuslineCommand();
43
47
  });
44
48
  program.command("update").description("Update coding-friend plugin, CLI, and statusline").option("--cli", "Update only the CLI (npm package)").option("--plugin", "Update only the Claude Code plugin").option("--statusline", "Update only the statusline").action(async (opts) => {
45
- const { updateCommand } = await import("./update-LA4B3LN4.js");
49
+ const { updateCommand } = await import("./update-EN6S3FKT.js");
46
50
  await updateCommand(opts);
47
51
  });
48
52
  var session = program.command("session").description("Save and load Claude Code sessions across machines");
@@ -57,11 +61,11 @@ session.command("save").description("Save current Claude Code session to sync fo
57
61
  "-s, --session-id <id>",
58
62
  "session UUID to save (default: auto-detect newest)"
59
63
  ).option("-l, --label <label>", "label for this session").action(async (opts) => {
60
- const { sessionSaveCommand } = await import("./session-74F7L5LV.js");
64
+ const { sessionSaveCommand } = await import("./session-STYOO4A4.js");
61
65
  await sessionSaveCommand(opts);
62
66
  });
63
67
  session.command("load").description("Load a saved session from sync folder").action(async () => {
64
- const { sessionLoadCommand } = await import("./session-74F7L5LV.js");
68
+ const { sessionLoadCommand } = await import("./session-STYOO4A4.js");
65
69
  await sessionLoadCommand();
66
70
  });
67
71
  var dev = program.command("dev").description("Development mode commands");
@@ -77,35 +81,35 @@ Dev subcommands:
77
81
  dev update [path] Update local dev plugin to latest version`
78
82
  );
79
83
  dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
80
- const { devOnCommand } = await import("./dev-2GBY3GKC.js");
84
+ const { devOnCommand } = await import("./dev-4NLCK6YG.js");
81
85
  await devOnCommand(path);
82
86
  });
83
87
  dev.command("off").description("Switch back to remote marketplace").action(async () => {
84
- const { devOffCommand } = await import("./dev-2GBY3GKC.js");
88
+ const { devOffCommand } = await import("./dev-4NLCK6YG.js");
85
89
  await devOffCommand();
86
90
  });
87
91
  dev.command("status").description("Show current dev mode").action(async () => {
88
- const { devStatusCommand } = await import("./dev-2GBY3GKC.js");
92
+ const { devStatusCommand } = await import("./dev-4NLCK6YG.js");
89
93
  await devStatusCommand();
90
94
  });
91
95
  dev.command("sync").description(
92
96
  "Copy local source files to plugin cache (no version bump needed)"
93
97
  ).action(async () => {
94
- const { devSyncCommand } = await import("./dev-2GBY3GKC.js");
98
+ const { devSyncCommand } = await import("./dev-4NLCK6YG.js");
95
99
  await devSyncCommand();
96
100
  });
97
101
  dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
98
102
  "[path]",
99
103
  "path to local coding-friend repo (default: saved path or cwd)"
100
104
  ).action(async (path) => {
101
- const { devRestartCommand } = await import("./dev-2GBY3GKC.js");
105
+ const { devRestartCommand } = await import("./dev-4NLCK6YG.js");
102
106
  await devRestartCommand(path);
103
107
  });
104
108
  dev.command("update").description("Update local dev plugin to latest version (off + on)").argument(
105
109
  "[path]",
106
110
  "path to local coding-friend repo (default: saved path or cwd)"
107
111
  ).action(async (path) => {
108
- const { devUpdateCommand } = await import("./dev-2GBY3GKC.js");
112
+ const { devUpdateCommand } = await import("./dev-4NLCK6YG.js");
109
113
  await devUpdateCommand(path);
110
114
  });
111
115
  program.parse();
@@ -1,3 +1,9 @@
1
+ import {
2
+ applyPermissions,
3
+ buildLearnDirRules,
4
+ getExistingRules,
5
+ getMissingRules
6
+ } from "./chunk-4C4EKYM3.js";
1
7
  import {
2
8
  BACK,
3
9
  applyDocsDirChange,
@@ -7,18 +13,18 @@ import {
7
13
  getScopeLabel,
8
14
  injectBackChoice,
9
15
  showConfigHint
10
- } from "./chunk-QQ5SVZET.js";
16
+ } from "./chunk-AHGBESGW.js";
11
17
  import {
12
18
  findStatuslineHookPath,
13
19
  isStatuslineConfigured,
14
20
  saveStatuslineConfig,
15
21
  selectStatuslineComponents,
16
22
  writeStatuslineSettings
17
- } from "./chunk-BPLN4LDL.js";
23
+ } from "./chunk-YAOUAJZP.js";
18
24
  import {
19
25
  ensureShellCompletion,
20
26
  hasShellCompletion
21
- } from "./chunk-7N64TDZ6.js";
27
+ } from "./chunk-BCYBFGVD.js";
22
28
  import {
23
29
  DEFAULT_CONFIG
24
30
  } from "./chunk-PGLUEN7D.js";
@@ -31,9 +37,8 @@ import {
31
37
  localConfigPath,
32
38
  mergeJson,
33
39
  readJson,
34
- resolvePath,
35
- writeJson
36
- } from "./chunk-TPRZHSFS.js";
40
+ resolvePath
41
+ } from "./chunk-HQ5BPWEV.js";
37
42
  import {
38
43
  log
39
44
  } from "./chunk-W5CD7WTX.js";
@@ -411,35 +416,23 @@ async function stepStatusline() {
411
416
  async function stepClaudePermissions(outputDir, autoCommit) {
412
417
  const resolved = resolvePath(outputDir);
413
418
  const homePath = resolved.startsWith(homedir()) ? resolved.replace(homedir(), "~") : resolved;
414
- const rules = [
415
- `Read(${homePath}/**)`,
416
- `Edit(${homePath}/**)`,
417
- `Write(${homePath}/**)`
418
- ];
419
- if (autoCommit) {
420
- rules.push(`Bash(cd ${homePath} && git add:*)`);
421
- rules.push(`Bash(cd ${homePath} && git commit:*)`);
422
- }
423
419
  const settingsPath = claudeSettingsPath();
424
- const settings = readJson(settingsPath);
425
- if (!settings) {
420
+ const existing = getExistingRules(settingsPath);
421
+ if (existing.length === 0 && !readJson(settingsPath)) {
426
422
  log.warn(
427
423
  "~/.claude/settings.json not found. Create it via Claude Code settings first."
428
424
  );
429
425
  return;
430
426
  }
431
- const permissions = settings.permissions ?? {};
432
- const existing = permissions.allow ?? [];
433
- const missing = rules.filter(
434
- (r) => !existing.some((e) => e === r || e.includes(homePath))
435
- );
427
+ const learnRules = buildLearnDirRules(homePath, autoCommit);
428
+ const missing = getMissingRules(existing, learnRules);
436
429
  if (missing.length === 0) {
437
430
  log.dim("All permission rules already configured.");
438
431
  return;
439
432
  }
440
433
  console.log("\nTo avoid repeated permission prompts, add these rules:");
441
434
  for (const r of missing) {
442
- console.log(` ${r}`);
435
+ console.log(` ${r.rule}`);
443
436
  }
444
437
  const ok = await confirm({
445
438
  message: "Add these to ~/.claude/settings.json?",
@@ -449,10 +442,13 @@ async function stepClaudePermissions(outputDir, autoCommit) {
449
442
  log.dim("Skipped. You'll get prompted each time.");
450
443
  return;
451
444
  }
452
- permissions.allow = [...existing, ...missing];
453
- settings.permissions = permissions;
454
- writeJson(settingsPath, settings);
445
+ applyPermissions(
446
+ settingsPath,
447
+ missing.map((r) => r.rule),
448
+ []
449
+ );
455
450
  log.success(`Added ${missing.length} permission rules.`);
451
+ log.dim("For all plugin permissions, run: `cf permission`");
456
452
  }
457
453
  async function initCommand() {
458
454
  _stepIndex = 0;
@@ -485,6 +481,12 @@ async function initCommand() {
485
481
  const docsDir = getDocsDir(updatedGlobal, updatedLocal);
486
482
  if (gitAvailable) {
487
483
  await stepGitignore(docsDir);
484
+ } else {
485
+ printStepHeader(
486
+ `Configure .gitignore ${chalk.dim("[skipped]")}`,
487
+ "Keeps AI-generated docs and config out of your git history."
488
+ );
489
+ log.dim("Skipped \u2014 not inside a git repo.");
488
490
  }
489
491
  await stepDocsLanguage(globalCfg, localCfg);
490
492
  const { outputDir, autoCommit, isExternal } = await stepLearnConfig(
@@ -500,6 +502,14 @@ async function initCommand() {
500
502
  "Grants Claude read/write access to your external learn folder without repeated prompts."
501
503
  );
502
504
  await stepClaudePermissions(outputDir, autoCommit);
505
+ } else {
506
+ printStepHeader(
507
+ `Configure Claude permissions ${chalk.dim("[skipped]")}`,
508
+ "Grants Claude read/write access to your external learn folder without repeated prompts."
509
+ );
510
+ log.dim(
511
+ "Skipped \u2014 learn directory is inside the project. Run `cf permission` for other permissions."
512
+ );
503
513
  }
504
514
  console.log();
505
515
  log.congrats("Setup complete!");
@@ -1,20 +1,20 @@
1
1
  import {
2
2
  getLatestVersion,
3
3
  semverCompare
4
- } from "./chunk-VYMXERKM.js";
4
+ } from "./chunk-UR5XUEH5.js";
5
5
  import {
6
6
  isMarketplaceRegistered
7
- } from "./chunk-HFLBFX6J.js";
7
+ } from "./chunk-TLLWOWYU.js";
8
8
  import {
9
9
  getInstalledVersion
10
- } from "./chunk-BPLN4LDL.js";
11
- import "./chunk-7N64TDZ6.js";
10
+ } from "./chunk-YAOUAJZP.js";
11
+ import "./chunk-BCYBFGVD.js";
12
12
  import "./chunk-PGLUEN7D.js";
13
13
  import {
14
14
  commandExists,
15
15
  run
16
16
  } from "./chunk-UFGNO6CW.js";
17
- import "./chunk-TPRZHSFS.js";
17
+ import "./chunk-HQ5BPWEV.js";
18
18
  import {
19
19
  log
20
20
  } from "./chunk-W5CD7WTX.js";
@@ -3,12 +3,12 @@ import {
3
3
  } from "./chunk-RZRT7NGT.js";
4
4
  import {
5
5
  resolveDocsDir
6
- } from "./chunk-FYHHNX7K.js";
6
+ } from "./chunk-EDVZPZ3B.js";
7
7
  import "./chunk-PGLUEN7D.js";
8
8
  import {
9
9
  run
10
10
  } from "./chunk-UFGNO6CW.js";
11
- import "./chunk-TPRZHSFS.js";
11
+ import "./chunk-HQ5BPWEV.js";
12
12
  import {
13
13
  log
14
14
  } from "./chunk-W5CD7WTX.js";
@@ -0,0 +1,168 @@
1
+ import {
2
+ PERMISSION_RULES,
3
+ applyPermissions,
4
+ getExistingRules,
5
+ groupByCategory
6
+ } from "./chunk-4C4EKYM3.js";
7
+ import {
8
+ claudeLocalSettingsPath
9
+ } from "./chunk-HQ5BPWEV.js";
10
+ import {
11
+ log
12
+ } from "./chunk-W5CD7WTX.js";
13
+
14
+ // src/commands/permission.ts
15
+ import { checkbox, confirm, select } from "@inquirer/prompts";
16
+ import chalk from "chalk";
17
+ import { homedir } from "os";
18
+ var TAG_COLORS = {
19
+ "[read-only]": chalk.green,
20
+ "[modify]": chalk.yellow,
21
+ "[write]": chalk.yellow,
22
+ "[remote]": chalk.red,
23
+ "[execute]": chalk.magenta,
24
+ "[network]": chalk.magenta
25
+ };
26
+ function colorDescription(desc) {
27
+ const parts = desc.split(" \xB7 ");
28
+ const main = parts[0];
29
+ const usedBy = parts.length > 1 ? chalk.dim("\xB7 " + parts.slice(1).join(" \xB7 ")) : "";
30
+ const tagMatch = main.match(/^(\[[^\]]+\])\s*(.*)/);
31
+ if (tagMatch) {
32
+ const colorFn = TAG_COLORS[tagMatch[1]] ?? chalk.cyan;
33
+ return `${colorFn(tagMatch[1])} ${tagMatch[2]} ${usedBy}`;
34
+ }
35
+ return `${main} ${usedBy}`;
36
+ }
37
+ async function interactiveFlow(allRules, existing) {
38
+ const groups = groupByCategory(allRules);
39
+ const allRuleStrings = allRules.map((r) => r.rule);
40
+ const managedExisting = existing.filter((r) => allRuleStrings.includes(r));
41
+ const enabled = new Set(managedExisting);
42
+ console.log(
43
+ chalk.dim(
44
+ "Full reference: https://cf.dinhanhthi.com/docs/reference/permissions/"
45
+ )
46
+ );
47
+ console.log();
48
+ let browsing = true;
49
+ while (browsing) {
50
+ const categoryChoices = [];
51
+ for (const [category, rules] of groups) {
52
+ const configured = rules.filter((r) => enabled.has(r.rule)).length;
53
+ const total = rules.length;
54
+ let suffix;
55
+ if (configured === total)
56
+ suffix = chalk.green(`${configured}/${total} \u2713`);
57
+ else if (configured > 0) suffix = chalk.yellow(`${configured}/${total}`);
58
+ else suffix = chalk.dim(`0/${total}`);
59
+ categoryChoices.push({
60
+ name: `${category} (${suffix})`,
61
+ value: category
62
+ });
63
+ }
64
+ categoryChoices.push({
65
+ name: chalk.green("\u2192 Apply changes"),
66
+ value: "__apply__"
67
+ });
68
+ const chosen = await select({
69
+ message: "Select a category to configure:",
70
+ choices: categoryChoices
71
+ });
72
+ if (chosen === "__apply__") {
73
+ browsing = false;
74
+ continue;
75
+ }
76
+ const categoryRules = groups.get(chosen);
77
+ const selected = await checkbox({
78
+ message: `${chosen} permissions:`,
79
+ choices: categoryRules.map((rule) => ({
80
+ name: rule.rule,
81
+ value: rule.rule,
82
+ checked: enabled.has(rule.rule),
83
+ description: colorDescription(rule.description)
84
+ }))
85
+ });
86
+ for (const rule of categoryRules) {
87
+ if (selected.includes(rule.rule)) {
88
+ enabled.add(rule.rule);
89
+ } else {
90
+ enabled.delete(rule.rule);
91
+ }
92
+ }
93
+ }
94
+ const toAdd = [...enabled].filter((r) => !existing.includes(r));
95
+ const toRemove = managedExisting.filter((r) => !enabled.has(r));
96
+ return { toAdd, toRemove };
97
+ }
98
+ async function permissionCommand(opts) {
99
+ console.log("=== \u{1F33F} Coding Friend Permissions \u{1F33F} ===");
100
+ console.log();
101
+ const settingsPath = claudeLocalSettingsPath();
102
+ const settingsLabel = settingsPath.replace(homedir(), "~");
103
+ const existing = getExistingRules(settingsPath);
104
+ const allRules = PERMISSION_RULES;
105
+ const allRuleStrings = allRules.map((r) => r.rule);
106
+ const managedExisting = existing.filter((r) => allRuleStrings.includes(r));
107
+ const unmanagedExisting = existing.filter((r) => !allRuleStrings.includes(r));
108
+ log.dim(`Settings: ${settingsLabel} (project-local)`);
109
+ log.dim(
110
+ `Current: ${managedExisting.length}/${allRules.length} Coding Friend rules configured`
111
+ );
112
+ if (unmanagedExisting.length > 0) {
113
+ log.dim(
114
+ `(${unmanagedExisting.length} other permission rules will not be touched)`
115
+ );
116
+ }
117
+ console.log();
118
+ if (opts.all) {
119
+ const recommended = allRules.filter((r) => r.recommended);
120
+ const toAdd2 = recommended.map((r) => r.rule).filter((r) => !existing.includes(r));
121
+ if (toAdd2.length === 0) {
122
+ log.success("All recommended permissions already configured.");
123
+ return;
124
+ }
125
+ console.log("Adding recommended permissions:");
126
+ for (const r of toAdd2) {
127
+ console.log(` ${chalk.green("+")} ${r}`);
128
+ }
129
+ console.log();
130
+ applyPermissions(settingsPath, toAdd2, []);
131
+ log.success(`Added ${toAdd2.length} permission rules.`);
132
+ return;
133
+ }
134
+ const { toAdd, toRemove } = await interactiveFlow(allRules, existing);
135
+ if (toAdd.length === 0 && toRemove.length === 0) {
136
+ log.dim("No changes.");
137
+ return;
138
+ }
139
+ console.log();
140
+ if (toAdd.length > 0) {
141
+ console.log(chalk.green(`Adding ${toAdd.length} rules:`));
142
+ for (const r of toAdd) {
143
+ console.log(` ${chalk.green("+")} ${r}`);
144
+ }
145
+ }
146
+ if (toRemove.length > 0) {
147
+ console.log(chalk.red(`Removing ${toRemove.length} rules:`));
148
+ for (const r of toRemove) {
149
+ console.log(` ${chalk.red("-")} ${r}`);
150
+ }
151
+ }
152
+ console.log();
153
+ const ok = await confirm({
154
+ message: `Apply changes to ${settingsLabel}?`,
155
+ default: true
156
+ });
157
+ if (!ok) {
158
+ log.dim("Skipped.");
159
+ return;
160
+ }
161
+ applyPermissions(settingsPath, toAdd, toRemove);
162
+ log.success(
163
+ `Done \u2014 added ${toAdd.length}, removed ${toRemove.length} rules.`
164
+ );
165
+ }
166
+ export {
167
+ permissionCommand
168
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureShellCompletion
4
- } from "./chunk-7N64TDZ6.js";
4
+ } from "./chunk-BCYBFGVD.js";
5
5
  import "./chunk-W5CD7WTX.js";
6
6
 
7
7
  // src/postinstall.ts
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  loadConfig
3
- } from "./chunk-FYHHNX7K.js";
3
+ } from "./chunk-EDVZPZ3B.js";
4
4
  import "./chunk-PGLUEN7D.js";
5
5
  import {
6
6
  claudeSessionDir,
7
7
  encodeProjectPath,
8
8
  readJson,
9
9
  writeJson
10
- } from "./chunk-TPRZHSFS.js";
10
+ } from "./chunk-HQ5BPWEV.js";
11
11
  import {
12
12
  log
13
13
  } from "./chunk-W5CD7WTX.js";
@@ -4,11 +4,11 @@ import {
4
4
  saveStatuslineConfig,
5
5
  selectStatuslineComponents,
6
6
  writeStatuslineSettings
7
- } from "./chunk-BPLN4LDL.js";
7
+ } from "./chunk-YAOUAJZP.js";
8
8
  import {
9
9
  ALL_COMPONENT_IDS
10
10
  } from "./chunk-PGLUEN7D.js";
11
- import "./chunk-TPRZHSFS.js";
11
+ import "./chunk-HQ5BPWEV.js";
12
12
  import {
13
13
  log
14
14
  } from "./chunk-W5CD7WTX.js";
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  isMarketplaceRegistered,
3
3
  isPluginInstalled
4
- } from "./chunk-HFLBFX6J.js";
4
+ } from "./chunk-TLLWOWYU.js";
5
5
  import {
6
6
  hasShellCompletion,
7
7
  removeShellCompletion
8
- } from "./chunk-7N64TDZ6.js";
8
+ } from "./chunk-BCYBFGVD.js";
9
9
  import {
10
10
  commandExists,
11
11
  run
@@ -18,7 +18,7 @@ import {
18
18
  marketplaceClonePath,
19
19
  readJson,
20
20
  writeJson
21
- } from "./chunk-TPRZHSFS.js";
21
+ } from "./chunk-HQ5BPWEV.js";
22
22
  import {
23
23
  log
24
24
  } from "./chunk-W5CD7WTX.js";
@@ -2,12 +2,12 @@ import {
2
2
  getLatestVersion,
3
3
  semverCompare,
4
4
  updateCommand
5
- } from "./chunk-VYMXERKM.js";
6
- import "./chunk-BPLN4LDL.js";
7
- import "./chunk-7N64TDZ6.js";
5
+ } from "./chunk-UR5XUEH5.js";
6
+ import "./chunk-YAOUAJZP.js";
7
+ import "./chunk-BCYBFGVD.js";
8
8
  import "./chunk-PGLUEN7D.js";
9
9
  import "./chunk-UFGNO6CW.js";
10
- import "./chunk-TPRZHSFS.js";
10
+ import "./chunk-HQ5BPWEV.js";
11
11
  import "./chunk-W5CD7WTX.js";
12
12
  export {
13
13
  getLatestVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {