clank-cli 0.1.65 → 0.1.66

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clank-cli",
3
3
  "description": "Keep AI files in a separate overlay repository",
4
- "version": "0.1.65",
4
+ "version": "0.1.66",
5
5
  "author": "",
6
6
  "type": "module",
7
7
  "bin": {
package/src/Cli.ts CHANGED
@@ -171,6 +171,7 @@ function registerUtilityCommands(program: Command): void {
171
171
  .command("vscode")
172
172
  .description("Generate VS Code settings to show clank files")
173
173
  .option("--remove", "Remove clank-generated VS Code settings")
174
+ .option("--force", "Generate even if settings.json is tracked by git")
174
175
  .action(withErrorHandling(vscodeCommand));
175
176
  }
176
177
 
@@ -40,7 +40,11 @@ import {
40
40
  isWorktreeInitialized,
41
41
  } from "../Templates.ts";
42
42
  import { findOrphans } from "./Check.ts";
43
- import { generateVscodeSettings, isVscodeProject } from "./VsCode.ts";
43
+ import {
44
+ checkVscodeTracking,
45
+ generateVscodeSettings,
46
+ isVscodeProject,
47
+ } from "./VsCode.ts";
44
48
 
45
49
  interface FileMapping extends TargetMapping {
46
50
  overlayPath: string;
@@ -291,6 +295,7 @@ async function maybeGenerateVscodeSettings(
291
295
  ): Promise<void> {
292
296
  const setting = config.vscodeSettings ?? "auto";
293
297
 
298
+ // User opted out - skip silently
294
299
  if (setting === "never") return;
295
300
 
296
301
  if (setting === "auto") {
@@ -298,7 +303,14 @@ async function maybeGenerateVscodeSettings(
298
303
  if (!isVscode) return;
299
304
  }
300
305
 
301
- // setting === "always" or (setting === "auto" && isVscodeProject)
306
+ // Check if settings.json is tracked and show appropriate warning
307
+ const check = await checkVscodeTracking(targetRoot);
308
+ if (!check.canGenerate) {
309
+ console.log(`\n${check.warning}`);
310
+ return;
311
+ }
312
+
313
+ // Generate: "always" mode, or "auto" with untracked/layered settings
302
314
  console.log("");
303
315
  await generateVscodeSettings(targetRoot);
304
316
  }
@@ -16,6 +16,95 @@ import {
16
16
 
17
17
  export interface VscodeOptions {
18
18
  remove?: boolean;
19
+ force?: boolean;
20
+ }
21
+
22
+ /** Result of checking if VS Code settings can be generated */
23
+ export interface VscodeTrackingCheck {
24
+ canGenerate: boolean;
25
+ warning?: string;
26
+ hasBase: boolean;
27
+ }
28
+
29
+ /** Check if settings.base.json exists */
30
+ export async function hasBaseSettings(targetRoot: string): Promise<boolean> {
31
+ return fileExists(join(targetRoot, ".vscode/settings.base.json"));
32
+ }
33
+
34
+ /** Read layered settings (base + local) if base exists */
35
+ async function readLayeredSettings(
36
+ targetRoot: string,
37
+ ): Promise<Record<string, unknown> | null> {
38
+ const basePath = join(targetRoot, ".vscode/settings.base.json");
39
+ const localPath = join(targetRoot, ".vscode/settings.local.json");
40
+
41
+ if (!(await fileExists(basePath))) return null;
42
+
43
+ let base: Record<string, unknown> = {};
44
+ const baseContent = await readFile(basePath, "utf-8");
45
+ try {
46
+ base = JSON.parse(baseContent);
47
+ } catch {
48
+ console.warn("Warning: Could not parse settings.base.json, ignoring");
49
+ return null;
50
+ }
51
+
52
+ let local: Record<string, unknown> = {};
53
+ if (await fileExists(localPath)) {
54
+ const localContent = await readFile(localPath, "utf-8");
55
+ try {
56
+ local = JSON.parse(localContent);
57
+ } catch {
58
+ console.warn("Warning: Could not parse settings.local.json, ignoring");
59
+ }
60
+ }
61
+
62
+ // Merge exclude patterns specially (combine, don't replace)
63
+ const baseSearch = (base["search.exclude"] as Record<string, boolean>) || {};
64
+ const localSearch =
65
+ (local["search.exclude"] as Record<string, boolean>) || {};
66
+ const baseFiles = (base["files.exclude"] as Record<string, boolean>) || {};
67
+ const localFiles = (local["files.exclude"] as Record<string, boolean>) || {};
68
+
69
+ return {
70
+ ...base,
71
+ ...local,
72
+ "search.exclude": { ...baseSearch, ...localSearch },
73
+ "files.exclude": { ...baseFiles, ...localFiles },
74
+ };
75
+ }
76
+
77
+ /** Check if settings.json is tracked and return appropriate warning */
78
+ export async function checkVscodeTracking(
79
+ targetRoot: string,
80
+ ): Promise<VscodeTrackingCheck> {
81
+ const settingsPath = join(targetRoot, ".vscode/settings.json");
82
+ const hasBase = await hasBaseSettings(targetRoot);
83
+ const isTracked = await isTrackedByGit(settingsPath, targetRoot);
84
+
85
+ if (!isTracked) {
86
+ return { canGenerate: true, hasBase };
87
+ }
88
+
89
+ // settings.json is tracked
90
+ if (hasBase) {
91
+ return {
92
+ canGenerate: false,
93
+ hasBase,
94
+ warning:
95
+ "settings.base.json found but settings.json is still tracked.\n" +
96
+ "Complete migration: git rm --cached .vscode/settings.json",
97
+ };
98
+ }
99
+
100
+ return {
101
+ canGenerate: false,
102
+ hasBase,
103
+ warning:
104
+ "Skipping: .vscode/settings.json is tracked.\n" +
105
+ "Use `clank vscode --force` to override, or migrate:\n" +
106
+ " mv .vscode/settings.json .vscode/settings.base.json && git rm --cached .vscode/settings.json",
107
+ };
19
108
  }
20
109
 
21
110
  /** Generate VS Code settings to show clank files in search and explorer */
@@ -27,6 +116,19 @@ export async function vscodeCommand(options?: VscodeOptions): Promise<void> {
27
116
  return;
28
117
  }
29
118
 
119
+ // Check if we can generate (tracking check)
120
+ const check = await checkVscodeTracking(targetRoot);
121
+ if (!check.canGenerate && !options?.force) {
122
+ console.log(check.warning);
123
+ return;
124
+ }
125
+
126
+ if (options?.force && !check.canGenerate) {
127
+ console.log(
128
+ "Note: settings.json is tracked, this will create uncommitted changes.\n",
129
+ );
130
+ }
131
+
30
132
  await generateVscodeSettings(targetRoot);
31
133
  }
32
134
 
@@ -70,10 +172,38 @@ export async function generateVscodeSettings(
70
172
  );
71
173
  }
72
174
 
175
+ /** Regenerate settings.json from base+local only (remove clank additions) */
176
+ async function removeLayeredSettings(
177
+ settingsPath: string,
178
+ layered: Record<string, unknown>,
179
+ ): Promise<void> {
180
+ delete layered["search.useIgnoreFiles"];
181
+
182
+ if (Object.keys(layered).length === 0) {
183
+ if (await fileExists(settingsPath)) {
184
+ await unlink(settingsPath);
185
+ console.log("Removed .vscode/settings.json (base+local were empty)");
186
+ }
187
+ } else {
188
+ await writeJsonFile(settingsPath, layered);
189
+ console.log(
190
+ "Regenerated .vscode/settings.json from base+local (removed clank patterns)",
191
+ );
192
+ }
193
+ }
194
+
73
195
  /** Remove clank-generated VS Code settings */
74
196
  export async function removeVscodeSettings(targetRoot: string): Promise<void> {
75
197
  const settingsPath = join(targetRoot, ".vscode/settings.json");
76
198
 
199
+ // If layered settings exist, regenerate from base+local only
200
+ const layered = await readLayeredSettings(targetRoot);
201
+ if (layered) {
202
+ await removeLayeredSettings(settingsPath, layered);
203
+ return;
204
+ }
205
+
206
+ // Legacy mode: selectively remove clank patterns
77
207
  if (!(await fileExists(settingsPath))) {
78
208
  return;
79
209
  }
@@ -123,11 +253,30 @@ export async function isVscodeProject(targetRoot: string): Promise<boolean> {
123
253
  }
124
254
  }
125
255
 
126
- /** Merge clank exclude patterns with existing .vscode/settings.json */
256
+ /** Merge clank exclude patterns with layered or existing settings */
127
257
  async function mergeVscodeSettings(
128
258
  targetRoot: string,
129
259
  excludePatterns: Record<string, boolean>,
130
260
  ): Promise<Record<string, unknown>> {
261
+ // Try layered settings first (base + local)
262
+ const layered = await readLayeredSettings(targetRoot);
263
+
264
+ if (layered) {
265
+ // Layered mode: base + local + clank
266
+ const layeredSearch =
267
+ (layered["search.exclude"] as Record<string, boolean>) || {};
268
+ const layeredFiles =
269
+ (layered["files.exclude"] as Record<string, boolean>) || {};
270
+
271
+ return {
272
+ ...layered,
273
+ "search.useIgnoreFiles": false,
274
+ "search.exclude": { ...layeredSearch, ...excludePatterns },
275
+ "files.exclude": { ...layeredFiles, ...excludePatterns },
276
+ };
277
+ }
278
+
279
+ // Legacy mode: read existing settings.json
131
280
  const settingsPath = join(targetRoot, ".vscode/settings.json");
132
281
 
133
282
  let existingSettings: Record<string, unknown> = {};
@@ -170,11 +319,17 @@ async function writeVscodeSettings(
170
319
  console.log(`Wrote ${settingsPath}`);
171
320
  }
172
321
 
173
- /** Add .vscode/settings.json to .git/info/exclude */
322
+ /** Add .vscode/settings.json and settings.local.json to .git/info/exclude */
174
323
  async function addVscodeToGitExclude(targetRoot: string): Promise<void> {
175
324
  const settingsPath = join(targetRoot, ".vscode/settings.json");
176
- if (await isTrackedByGit(settingsPath, targetRoot)) return;
177
- await addToGitExclude(targetRoot, ".vscode/settings.json");
325
+ const localPath = join(targetRoot, ".vscode/settings.local.json");
326
+
327
+ if (!(await isTrackedByGit(settingsPath, targetRoot))) {
328
+ await addToGitExclude(targetRoot, ".vscode/settings.json");
329
+ }
330
+ if (!(await isTrackedByGit(localPath, targetRoot))) {
331
+ await addToGitExclude(targetRoot, ".vscode/settings.local.json");
332
+ }
178
333
  }
179
334
 
180
335
  /** Remove patterns from an exclude object, deleting the key if empty */