@fnclaude/cli 0.7.8 → 1.0.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.
@@ -11,6 +11,7 @@
11
11
  import { readFileSync } from 'node:fs';
12
12
  import { homedir, platform } from 'node:os';
13
13
  import { join } from 'node:path';
14
+ import { errorMessage } from './errors.js';
14
15
 
15
16
  /**
16
17
  * fnclaude's view of the shared `repoSettings` block. Only the keys
@@ -80,7 +81,7 @@ export function mergeRepoSettings(paths: string[]): LoadRepoSettingsResult {
80
81
  const warnings: string[] = [];
81
82
  for (const p of paths) {
82
83
  const { settings: f, warning } = readRepoSettings(p);
83
- if (warning !== null) warnings.push(warning);
84
+ if (warning !== undefined) warnings.push(warning);
84
85
  if (!f) continue;
85
86
  // Shallow-merge per field: only overwrite when the higher tier sets
86
87
  // a non-empty value.
@@ -93,8 +94,8 @@ export function mergeRepoSettings(paths: string[]): LoadRepoSettingsResult {
93
94
  }
94
95
 
95
96
  interface ReadRepoSettingsResult {
96
- settings: RepoSettings | null;
97
- warning: string | null;
97
+ settings: RepoSettings | undefined;
98
+ warning: string | undefined;
98
99
  }
99
100
 
100
101
  function readRepoSettings(path: string): ReadRepoSettingsResult {
@@ -103,25 +104,25 @@ function readRepoSettings(path: string): ReadRepoSettingsResult {
103
104
  data = readFileSync(path, 'utf8');
104
105
  } catch {
105
106
  // Missing file is the common path — stay silent.
106
- return { settings: null, warning: null };
107
+ return { settings: undefined, warning: undefined };
107
108
  }
108
109
  let f: SettingsFile;
109
110
  try {
110
111
  f = JSON.parse(data) as SettingsFile;
111
112
  } catch (err) {
112
113
  return {
113
- settings: null,
114
- warning: `fnclaude: repo-settings file ${path} is malformed, skipping: ${(err as Error).message}`,
114
+ settings: undefined,
115
+ warning: `fnclaude: repo-settings file ${path} is malformed, skipping: ${errorMessage(err)}`,
115
116
  };
116
117
  }
117
- return { settings: f.repoSettings ?? null, warning: null };
118
+ return { settings: f.repoSettings ?? undefined, warning: undefined };
118
119
  }
119
120
 
120
121
  /**
121
122
  * Platform-specific path to Claude Code's managed-settings.json, or
122
- * `null` on platforms with no such convention.
123
+ * `undefined` on platforms with no such convention.
123
124
  */
124
- export function managedSettingsPath(): string | null {
125
+ export function managedSettingsPath(): string | undefined {
125
126
  switch (platform()) {
126
127
  case 'linux':
127
128
  return '/etc/claude-code/managed-settings.json';
@@ -130,10 +131,10 @@ export function managedSettingsPath(): string | null {
130
131
  case 'win32': {
131
132
  const pd = process.env.ProgramData;
132
133
  if (pd) return join(pd, 'ClaudeCode', 'managed-settings.json');
133
- return null;
134
+ return undefined;
134
135
  }
135
136
  default:
136
- return null;
137
+ return undefined;
137
138
  }
138
139
  }
139
140
 
package/src/sanitize.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // '/' is allowed so git-style nested refs (feat/foo, team/x/y/z) pass
8
8
  // through and produce nested worktree paths.
9
9
  //
10
- // Returns null when:
10
+ // Returns undefined when:
11
11
  // - the input is empty
12
12
  // - the input starts with '/' (would escape the configured path prefix)
13
13
  // - the result reduces to empty after sanitization
@@ -21,9 +21,9 @@ const RE_PATH_SAFE_BAD = /[^A-Za-z0-9._/-]+/g;
21
21
  const RE_DASH_RUN = /-{2,}/g;
22
22
  const RE_SLASH_RUN = /\/{2,}/g;
23
23
 
24
- export function sanitizeName(s: string): string | null {
25
- if (s === '') return null;
26
- if (s.startsWith('/')) return null;
24
+ export function sanitizeName(s: string): string | undefined {
25
+ if (s === '') return undefined;
26
+ if (s.startsWith('/')) return undefined;
27
27
 
28
28
  let out = s.replace(RE_PATH_SAFE_BAD, '-');
29
29
  out = out.replace(RE_DASH_RUN, '-');
@@ -31,8 +31,8 @@ export function sanitizeName(s: string): string | null {
31
31
  out = trimLeftAny(out, '-.');
32
32
  out = trimRightAny(out, '-/');
33
33
 
34
- if (out === '') return null;
35
- if (out.includes('..')) return null;
34
+ if (out === '') return undefined;
35
+ if (out.includes('..')) return undefined;
36
36
  return out;
37
37
  }
38
38
 
@@ -71,7 +71,7 @@ export function sanitizeNamesInPassthrough(p: readonly string[]): SanitizeNamesR
71
71
  if ((t === '--name' || t === '-n') && i + 1 < out.length) {
72
72
  const val = out[i + 1]!;
73
73
  const decision = decideSanitize(val);
74
- if (decision.warning !== null) warnings.push(decision.warning);
74
+ if (decision.warning !== undefined) warnings.push(decision.warning);
75
75
  if (decision.replace) out[i + 1] = decision.cleaned;
76
76
  i++; // skip the value slot
77
77
  continue;
@@ -79,14 +79,14 @@ export function sanitizeNamesInPassthrough(p: readonly string[]): SanitizeNamesR
79
79
  if (t.startsWith('--name=')) {
80
80
  const val = t.slice('--name='.length);
81
81
  const decision = decideSanitize(val);
82
- if (decision.warning !== null) warnings.push(decision.warning);
82
+ if (decision.warning !== undefined) warnings.push(decision.warning);
83
83
  if (decision.replace) out[i] = `--name=${decision.cleaned}`;
84
84
  continue;
85
85
  }
86
86
  if (t.startsWith('-n=')) {
87
87
  const val = t.slice('-n='.length);
88
88
  const decision = decideSanitize(val);
89
- if (decision.warning !== null) warnings.push(decision.warning);
89
+ if (decision.warning !== undefined) warnings.push(decision.warning);
90
90
  if (decision.replace) out[i] = `-n=${decision.cleaned}`;
91
91
  continue;
92
92
  }
@@ -96,13 +96,13 @@ export function sanitizeNamesInPassthrough(p: readonly string[]): SanitizeNamesR
96
96
 
97
97
  interface SanitizeDecision {
98
98
  cleaned: string;
99
- warning: string | null;
99
+ warning: string | undefined;
100
100
  replace: boolean;
101
101
  }
102
102
 
103
103
  function decideSanitize(val: string): SanitizeDecision {
104
104
  const cleaned = sanitizeName(val);
105
- if (cleaned === null) {
105
+ if (cleaned === undefined) {
106
106
  return {
107
107
  cleaned: val,
108
108
  warning: `fnclaude: --name ${JSON.stringify(val)} has no path-safe characters; passing through unchanged`,
@@ -110,7 +110,7 @@ function decideSanitize(val: string): SanitizeDecision {
110
110
  };
111
111
  }
112
112
  if (cleaned === val) {
113
- return { cleaned: '', warning: null, replace: false };
113
+ return { cleaned: '', warning: undefined, replace: false };
114
114
  }
115
115
  return {
116
116
  cleaned,
@@ -87,7 +87,7 @@ export function readLivePermissionMode(
87
87
  } catch {
88
88
  continue; // malformed line — ignore
89
89
  }
90
- if (r.type === 'permission-mode' && typeof r.permissionMode === 'string' && r.permissionMode !== '') {
90
+ if (r.type === 'permission-mode' && typeof r.permissionMode === 'string' && r.permissionMode) {
91
91
  latest = r.permissionMode;
92
92
  }
93
93
  }
@@ -110,10 +110,10 @@ export interface RestartReminderOverrides {
110
110
 
111
111
  /**
112
112
  * Read the trailing entries of `data` and return the most recent `uuid`
113
- * field, or `null` if none found. Used to link the appended reminder into
114
- * the JSONL parent-chain.
113
+ * field, or `undefined` if none found. Used to link the appended reminder
114
+ * into the JSONL parent-chain.
115
115
  */
116
- function lastEntryUUID(data: string): string | null {
116
+ function lastEntryUUID(data: string): string | undefined {
117
117
  const lines = data.split('\n');
118
118
  for (let i = lines.length - 1; i >= 0; i--) {
119
119
  const line = lines[i];
@@ -128,7 +128,7 @@ function lastEntryUUID(data: string): string | null {
128
128
  return parsed.uuid;
129
129
  }
130
130
  }
131
- return null;
131
+ return undefined;
132
132
  }
133
133
 
134
134
  /** Render the system-reminder body text, optionally naming overrides. */
@@ -136,16 +136,16 @@ export function renderRestartReminderContent(
136
136
  overrides?: RestartReminderOverrides,
137
137
  ): string {
138
138
  const parts: string[] = [];
139
- if (overrides?.model && overrides.model !== '') {
139
+ if (overrides?.model) {
140
140
  parts.push(`model swap to ${overrides.model}`);
141
141
  }
142
- if (overrides?.effort && overrides.effort !== '') {
142
+ if (overrides?.effort) {
143
143
  parts.push(`effort=${overrides.effort}`);
144
144
  }
145
- if (overrides?.permissionMode && overrides.permissionMode !== '') {
145
+ if (overrides?.permissionMode) {
146
146
  parts.push(`permission-mode=${overrides.permissionMode}`);
147
147
  }
148
- if (overrides?.agent && overrides.agent !== '') {
148
+ if (overrides?.agent) {
149
149
  parts.push(`agent=${overrides.agent}`);
150
150
  }
151
151
  if (overrides?.ide) {
@@ -192,7 +192,9 @@ export function appendRestartReminder(
192
192
  // fresh anyway, so the reminder would be off-target.
193
193
  return;
194
194
  }
195
- const parentUuid = lastEntryUUID(existing);
195
+ // JSONL parentUuid is on the wire — keep null encoding for "no parent"
196
+ // entries to match Claude Code's own writer.
197
+ const parentUuid = lastEntryUUID(existing) ?? null;
196
198
  const entry = {
197
199
  parentUuid,
198
200
  isSidechain: false,
package/src/worktree.ts CHANGED
@@ -114,15 +114,15 @@ export function listWorktrees(dir: string, runner: GitRunner = defaultGitRunner)
114
114
  * 3. Basename of the path == query (last-resort fallback for worktrees
115
115
  * whose branch was renamed or whose creator skipped the convention)
116
116
  *
117
- * Returns null when no entry matches. Undefined `query` short-circuits to
118
- * null so that detached worktrees (branch=undefined) can't be matched by
119
- * accident.
117
+ * Returns undefined when no entry matches. Undefined `query` short-circuits
118
+ * to undefined so that detached worktrees (branch=undefined) can't be
119
+ * matched by accident.
120
120
  */
121
121
  export function findWorktree(
122
122
  worktrees: readonly WorktreeInfo[],
123
123
  query: string | undefined,
124
- ): WorktreeInfo | null {
125
- if (query === undefined) return null;
124
+ ): WorktreeInfo | undefined {
125
+ if (query === undefined) return undefined;
126
126
 
127
127
  for (const wt of worktrees) {
128
128
  if (wt.branch === query) return wt;
@@ -133,7 +133,7 @@ export function findWorktree(
133
133
  for (const wt of worktrees) {
134
134
  if (basename(wt.path) === query) return wt;
135
135
  }
136
- return null;
136
+ return undefined;
137
137
  }
138
138
 
139
139
  function stripWorktreePrefix(branch: string): string {