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 +1 -1
- package/src/Cli.ts +1 -0
- package/src/commands/Link.ts +14 -2
- package/src/commands/VsCode.ts +159 -4
package/package.json
CHANGED
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
|
|
package/src/commands/Link.ts
CHANGED
|
@@ -40,7 +40,11 @@ import {
|
|
|
40
40
|
isWorktreeInitialized,
|
|
41
41
|
} from "../Templates.ts";
|
|
42
42
|
import { findOrphans } from "./Check.ts";
|
|
43
|
-
import {
|
|
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
|
-
//
|
|
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
|
}
|
package/src/commands/VsCode.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
177
|
-
|
|
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 */
|