Skip to content

Commit 506e38a

Browse files
authored
Add Global Ignore (#118)
* Make copying graceful * Graceful copying * Tick version * Absolute imports * Format code * Graceful deletions during checkout * Use graceful copy for pull * Add global vtignore * Make pull also use graceful delete * Add command to edit global ignore * Fix default global vtignore location * Remove unused import * Make deno publish happy * Tick version
1 parent 69a8c83 commit 506e38a

File tree

7 files changed

+134
-12
lines changed

7 files changed

+134
-12
lines changed

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://raw.githubusercontent.com/denoland/deno/348900b8b79f4a434cab4c74b3bc8d4d2fa8ee74/cli/schemas/config-file.v1.json",
33
"name": "@valtown/vt",
44
"description": "The Val Town CLI",
5-
"version": "0.1.35",
5+
"version": "0.1.36",
66
"exports": "./vt.ts",
77
"license": "MIT",
88
"tasks": {

src/cmd/lib/config.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Command } from "@cliffy/command";
2-
import VTConfig from "~/vt/VTConfig.ts";
2+
import VTConfig, { globalConfig } from "~/vt/VTConfig.ts";
33
import { findVtRoot } from "~/vt/vt/utils.ts";
44
import { doWithSpinner } from "~/cmd/utils.ts";
55
import { getNestedProperty, setNestedProperty } from "~/utils.ts";
@@ -10,6 +10,11 @@ import { printYaml } from "~/cmd/styles.ts";
1010
import { fromError } from "zod-validation-error";
1111
import z from "zod";
1212
import { colors } from "@cliffy/ansi/colors";
13+
import { DEFAULT_WRAP_AMOUNT, GLOBAL_VT_CONFIG_PATH } from "~/consts.ts";
14+
import { join } from "@std/path";
15+
import wrap from "word-wrap";
16+
import { openEditorAt } from "~/cmd/lib/utils/openEditorAt.ts";
17+
import { Select } from "@cliffy/prompt";
1318

1419
function showConfigOptions() {
1520
// deno-lint-ignore no-explicit-any
@@ -37,6 +42,32 @@ function showConfigOptions() {
3742
printYaml(stringifyYaml(jsonSchema["properties"]));
3843
}
3944

45+
export const configWhereCmd = new Command()
46+
.name("where")
47+
.description("Show the config file locations")
48+
.action(async () => {
49+
// Find project root, if in a Val Town project
50+
let vtRoot: string | undefined = undefined;
51+
try {
52+
vtRoot = await findVtRoot(Deno.cwd());
53+
} catch (_) {
54+
// ignore not found
55+
}
56+
57+
// Local config is always in <root>/.vt/config.yaml
58+
const localConfigPath = vtRoot
59+
? join(vtRoot, ".vt", "config.yaml")
60+
: undefined;
61+
62+
// Just print the resolved paths, always global first, then local if it exists
63+
if (GLOBAL_VT_CONFIG_PATH) {
64+
console.log(GLOBAL_VT_CONFIG_PATH);
65+
}
66+
if (localConfigPath) {
67+
console.log(localConfigPath);
68+
}
69+
});
70+
4071
export const configSetCmd = new Command()
4172
.description("Set a configuration value")
4273
.option("--local", "Set in the local configuration (val-specific)")
@@ -60,11 +91,11 @@ export const configSetCmd = new Command()
6091

6192
const config = await vtConfig.loadConfig();
6293
const updatedConfig = setNestedProperty(config, key, value);
63-
const oldProperty = getNestedProperty(config, key) as
94+
const oldProperty = getNestedProperty(config, key, null) as
6495
| string
65-
| undefined;
96+
| null;
6697

67-
if (oldProperty && oldProperty === value) {
98+
if (oldProperty !== null && oldProperty.toString() === value) {
6899
throw new Error(
69100
`Property ${colors.bold(key)} is already set to ${
70101
colors.bold(oldProperty)
@@ -98,7 +129,9 @@ export const configSetCmd = new Command()
98129
if (e instanceof z.ZodError) {
99130
throw new Error(
100131
"Invalid input provided! \n" +
101-
colors.red(fromError(e).toString()),
132+
wrap(colors.red(fromError(e).toString()), {
133+
width: DEFAULT_WRAP_AMOUNT,
134+
}),
102135
);
103136
} else throw e;
104137
}
@@ -110,6 +143,8 @@ export const configGetCmd = new Command()
110143
.description("Get a configuration value")
111144
.arguments("[key]")
112145
.alias("show")
146+
.example("Display current configuration", "vt config get")
147+
.example("Display the API key", "vt config get apiKey")
113148
.action(async (_: unknown, key?: string) => {
114149
await doWithSpinner("Retreiving configuration...", async (spinner) => {
115150
// Check if we're in a Val Town Val directory
@@ -140,6 +175,41 @@ export const configGetCmd = new Command()
140175
});
141176
});
142177

178+
export const configIgnoreCmd = new Command()
179+
.name("ignore")
180+
.description("Edit or display the global vtignore file")
181+
.option("--no-editor", "Do not open the editor, just display the file path")
182+
.action(async ({ editor }: { editor?: boolean }) => {
183+
const { globalIgnoreFiles } = await globalConfig.loadConfig();
184+
185+
if (!globalIgnoreFiles || globalIgnoreFiles.length === 0) {
186+
console.log("No global ignore files found");
187+
Deno.exit(1);
188+
}
189+
190+
let globalIgnorePath: string;
191+
192+
if (globalIgnoreFiles.length === 1) {
193+
globalIgnorePath = globalIgnoreFiles[0];
194+
} else {
195+
// Use Select prompt if multiple files are available
196+
globalIgnorePath = await Select.prompt({
197+
message: "Select a vtignore file to edit or display",
198+
options: globalIgnoreFiles.map((file) => ({ name: file, value: file })),
199+
});
200+
}
201+
202+
if (!editor) console.log(globalIgnorePath);
203+
else {
204+
const editor = Deno.env.get("EDITOR");
205+
if (editor) {
206+
await openEditorAt(globalIgnorePath);
207+
} else {
208+
console.log(globalIgnorePath);
209+
}
210+
}
211+
});
212+
143213
export const configOptionsCmd = new Command()
144214
.name("options")
145215
.description("List all available configuration options")
@@ -152,4 +222,6 @@ export const configCmd = new Command()
152222
.description("Manage vt configuration")
153223
.command("set", configSetCmd)
154224
.command("get", configGetCmd)
225+
.command("ignore", configIgnoreCmd)
226+
.command("where", configWhereCmd)
155227
.command("options", configOptionsCmd);

src/cmd/lib/utils/openEditorAt.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Opens a file at the specified path using the editor defined in the EDITOR environment variable.
3+
* If the EDITOR variable is not set, it simply prints the file path.
4+
*
5+
* @param {string} filePath - The path to the file that should be opened in the editor.
6+
*/
7+
export async function openEditorAt(filePath: string) {
8+
const editor = Deno.env.get("EDITOR");
9+
10+
if (editor) {
11+
const process = new Deno.Command(editor, {
12+
args: [filePath],
13+
stdin: "inherit",
14+
stdout: "inherit",
15+
stderr: "inherit",
16+
});
17+
18+
const { status } = process.spawn();
19+
if (!(await status).success) {
20+
console.log(`Failed to open editor ${editor}`);
21+
}
22+
} else {
23+
console.log(filePath);
24+
}
25+
}

src/consts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export const GLOBAL_VT_CONFIG_PATH = join(xdg.config(), PROGRAM_NAME);
3030
export const DEFAULT_WRAP_WIDTH = 80;
3131
export const MAX_WALK_UP_LEVELS = 100;
3232

33+
export const DEFAULT_WRAP_AMOUNT = 80;
34+
3335
export const FIRST_VERSION_NUMBER = 0;
3436

3537
export const STATUS_STYLES: Record<

src/vt/lib/pull.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export function pull(params: PullParams): Promise<PushResult> {
131131
if (!(e instanceof Deno.errors.NotFound)) throw e;
132132
}
133133
}));
134+
134135
return [{ itemStateChanges: changes }, !dryRun];
135136
},
136137
{ targetDir, prefix: "vt_pull_" },

src/vt/vt/VTMeta.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import {
55
META_STATE_FILE_NAME,
66
} from "~/consts.ts";
77
import { ALWAYS_IGNORE_PATTERNS } from "~/consts.ts";
8-
import * as path from "@std/path";
98
import { VTStateSchema } from "~/vt/vt/schemas.ts";
109
import type { z } from "zod";
1110
import { ensureDir, exists, walk } from "@std/fs";
11+
import { globalConfig } from "~/vt/VTConfig.ts";
12+
import { basename, join } from "@std/path";
1213

1314
/**
1415
* The VTMeta class manages .vt/* configuration files and provides abstractions
@@ -33,7 +34,7 @@ export default class VTMeta {
3334
* @returns The full file path as a string.
3435
*/
3536
public getVtStateFileName(): string {
36-
return path.join(this.#rootPath, META_FOLDER_NAME, META_STATE_FILE_NAME);
37+
return join(this.#rootPath, META_FOLDER_NAME, META_STATE_FILE_NAME);
3738
}
3839

3940
/**
@@ -44,15 +45,21 @@ export default class VTMeta {
4445
private async gitignoreFilePaths(): Promise<string[]> {
4546
const ignoreFiles: string[] = [];
4647

48+
// Always add the global .vtignore if it exists
49+
const { globalIgnoreFiles } = await globalConfig.loadConfig();
50+
for (const filePath of globalIgnoreFiles || []) {
51+
if (await exists(filePath)) ignoreFiles.push(filePath);
52+
}
53+
4754
// Walk through all directories recursively starting from root path
4855
for await (const file of walk(this.#rootPath)) {
49-
if (path.basename(file.path) === META_IGNORE_FILE_NAME) {
56+
if (basename(file.path) === META_IGNORE_FILE_NAME) {
5057
if (await exists(file.path)) ignoreFiles.push(file.path);
5158
}
5259
}
5360

5461
// Always include the root meta ignore file if it wasn't found in the walk
55-
const rootMetaIgnore = path.join(this.#rootPath, META_IGNORE_FILE_NAME);
62+
const rootMetaIgnore = join(this.#rootPath, META_IGNORE_FILE_NAME);
5663
if (!ignoreFiles.includes(rootMetaIgnore) && await exists(rootMetaIgnore)) {
5764
ignoreFiles.push(rootMetaIgnore);
5865
}
@@ -99,7 +106,7 @@ export default class VTMeta {
99106
});
100107

101108
// Ensure the metadata directory exists
102-
await ensureDir(path.join(this.#rootPath, META_FOLDER_NAME));
109+
await ensureDir(join(this.#rootPath, META_FOLDER_NAME));
103110

104111
// Write the meta to file
105112
await Deno.writeTextFile(

src/vt/vt/schemas.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { join } from "@std/path";
12
import { z } from "zod";
2-
import { DEFAULT_EDITOR_TEMPLATE } from "~/consts.ts";
3+
import {
4+
DEFAULT_EDITOR_TEMPLATE,
5+
GLOBAL_VT_CONFIG_PATH,
6+
META_IGNORE_FILE_NAME,
7+
} from "~/consts.ts";
38

49
/**
510
* JSON schema for the state.json file for the .vt folder.
@@ -43,6 +48,15 @@ export const VTConfigSchema = z.object({
4348
message: "API key must be 32-33 characters long when provided",
4449
})
4550
.nullable(),
51+
globalIgnoreFiles: z.preprocess(
52+
(input) => {
53+
if (typeof input === "string") {
54+
return input.split(",").map((s) => s.trim()).filter(Boolean);
55+
}
56+
return input;
57+
},
58+
z.array(z.string()),
59+
).optional(),
4660
dangerousOperations: z.object({
4761
confirmation: z.union([
4862
z.boolean(),
@@ -54,6 +68,7 @@ export const VTConfigSchema = z.object({
5468

5569
export const DefaultVTConfig: z.infer<typeof VTConfigSchema> = {
5670
apiKey: null,
71+
globalIgnoreFiles: [join(GLOBAL_VT_CONFIG_PATH, META_IGNORE_FILE_NAME)],
5772
dangerousOperations: {
5873
confirmation: true,
5974
},

0 commit comments

Comments
 (0)