@fresh-editor/fresh-editor 0.3.7 → 0.3.9
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/CHANGELOG.md +97 -0
- package/README.md +0 -1
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +28 -0
- package/plugins/audit_mode.ts +34 -3
- package/plugins/config-schema.json +7 -0
- package/plugins/dashboard.ts +3 -3
- package/plugins/env-manager.ts +168 -0
- package/plugins/examples/bookmarks.ts +3 -2
- package/plugins/git_log.ts +58 -75
- package/plugins/lib/finder.ts +57 -10
- package/plugins/lib/fresh.d.ts +150 -3
- package/plugins/lib/widgets.ts +8 -0
- package/plugins/live_diff.ts +12 -17
- package/plugins/live_grep.i18n.json +349 -27
- package/plugins/live_grep.ts +596 -141
- package/plugins/orchestrator.ts +1506 -534
- package/plugins/pkg.ts +168 -3
- package/plugins/schemas/theme.schema.json +53 -14
- package/plugins/theme_editor.i18n.json +84 -84
- package/plugins/tsconfig.json +1 -0
- package/plugins/vi_mode.ts +3 -3
- package/themes/light.json +1 -1
- package/themes/terminal.json +3 -3
package/plugins/pkg.ts
CHANGED
|
@@ -232,6 +232,12 @@ interface ParsedPackageUrl {
|
|
|
232
232
|
name: string;
|
|
233
233
|
/** Whether this is a local file path (not a remote URL) */
|
|
234
234
|
isLocal: boolean;
|
|
235
|
+
/**
|
|
236
|
+
* Whether the URL points directly to a single downloadable file (e.g. a
|
|
237
|
+
* theme JSON) rather than to a git repository. When true, the URL should
|
|
238
|
+
* be fetched over HTTP instead of cloned with git.
|
|
239
|
+
*/
|
|
240
|
+
isDirectFile: boolean;
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
// =============================================================================
|
|
@@ -296,6 +302,24 @@ function isLocalPath(str: string): boolean {
|
|
|
296
302
|
return false;
|
|
297
303
|
}
|
|
298
304
|
|
|
305
|
+
/**
|
|
306
|
+
* File extensions that indicate a URL points to a single downloadable file
|
|
307
|
+
* (e.g. a Fresh theme JSON) rather than to a git repository.
|
|
308
|
+
*/
|
|
309
|
+
const DIRECT_FILE_EXTENSIONS = /\.(jsonc?|JSONC?)$/;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if a URL points directly to a single downloadable file rather than
|
|
313
|
+
* to a git repository. Examples:
|
|
314
|
+
* - `https://github.com/user-attachments/files/123/synthwave-84.json` → true
|
|
315
|
+
* - `https://github.com/user/repo` → false
|
|
316
|
+
*/
|
|
317
|
+
function isDirectFileUrl(url: string): boolean {
|
|
318
|
+
// Strip query string for extension check
|
|
319
|
+
const pathOnly = url.split("?")[0];
|
|
320
|
+
return DIRECT_FILE_EXTENSIONS.test(pathOnly);
|
|
321
|
+
}
|
|
322
|
+
|
|
299
323
|
/**
|
|
300
324
|
* Parse a package URL that may contain a subpath fragment.
|
|
301
325
|
*
|
|
@@ -305,6 +329,7 @@ function isLocalPath(str: string): boolean {
|
|
|
305
329
|
* - `https://github.com/user/repo.git#packages/my-plugin` - with .git suffix
|
|
306
330
|
* - `/path/to/local/repo#subdir` - local path with subpath
|
|
307
331
|
* - `/path/to/local/package` - direct local package path
|
|
332
|
+
* - `https://example.com/path/to/theme.json` - direct file (downloaded over HTTP)
|
|
308
333
|
*
|
|
309
334
|
* The fragment (after #) specifies a subdirectory within the repo.
|
|
310
335
|
*/
|
|
@@ -329,19 +354,28 @@ function parsePackageUrl(url: string): ParsedPackageUrl {
|
|
|
329
354
|
// Determine if this is a local path
|
|
330
355
|
const isLocal = isLocalPath(repoUrl);
|
|
331
356
|
|
|
357
|
+
// A remote URL ending in a recognized file extension (and without a
|
|
358
|
+
// subpath fragment) is treated as a direct file download, not a git repo.
|
|
359
|
+
const isDirectFile = !isLocal && !subpath && isDirectFileUrl(repoUrl);
|
|
360
|
+
|
|
332
361
|
// Extract package name
|
|
333
362
|
let name: string;
|
|
334
363
|
if (subpath) {
|
|
335
364
|
// For monorepo/directory, use the last component of the subpath
|
|
336
365
|
const parts = subpath.split("/");
|
|
337
366
|
name = parts[parts.length - 1].replace(/^fresh-/, "");
|
|
367
|
+
} else if (isDirectFile) {
|
|
368
|
+
// For direct file downloads, use the filename without the extension
|
|
369
|
+
const pathOnly = repoUrl.split("?")[0];
|
|
370
|
+
const match = pathOnly.match(/\/([^\/]+?)\.(jsonc?|JSONC?)$/);
|
|
371
|
+
name = match ? match[1].replace(/^fresh-/, "") : "unknown";
|
|
338
372
|
} else {
|
|
339
373
|
// For regular repo/path, use the last component
|
|
340
374
|
const match = repoUrl.match(/\/([^\/]+?)(\.git)?$/);
|
|
341
375
|
name = match ? match[1].replace(/^fresh-/, "") : "unknown";
|
|
342
376
|
}
|
|
343
377
|
|
|
344
|
-
return { repoUrl, subpath, name, isLocal };
|
|
378
|
+
return { repoUrl, subpath, name, isLocal, isDirectFile };
|
|
345
379
|
}
|
|
346
380
|
|
|
347
381
|
/**
|
|
@@ -784,11 +818,12 @@ function validatePackage(packageDir: string, packageName: string): ValidationRes
|
|
|
784
818
|
}
|
|
785
819
|
|
|
786
820
|
/**
|
|
787
|
-
* Install a package from git URL or local path.
|
|
821
|
+
* Install a package from a git URL, direct file URL, or local path.
|
|
788
822
|
*
|
|
789
823
|
* Supports:
|
|
790
824
|
* - `https://github.com/user/repo` - standard git repo
|
|
791
825
|
* - `https://github.com/user/repo#packages/my-plugin` - monorepo with subpath
|
|
826
|
+
* - `https://example.com/path/to/theme.json` - direct file download (theme JSON)
|
|
792
827
|
* - `/path/to/local/repo#subdir` - local path with subpath
|
|
793
828
|
* - `/path/to/local/package` - direct local package path
|
|
794
829
|
*
|
|
@@ -808,6 +843,9 @@ async function installPackage(
|
|
|
808
843
|
if (parsed.isLocal) {
|
|
809
844
|
// Local path installation: copy directly
|
|
810
845
|
return await installFromLocalPath(parsed, packageName);
|
|
846
|
+
} else if (parsed.isDirectFile) {
|
|
847
|
+
// Direct file URL (e.g. theme JSON): download with curl, not git clone
|
|
848
|
+
return await installFromDirectFile(parsed.repoUrl, packageName);
|
|
811
849
|
} else if (parsed.subpath) {
|
|
812
850
|
// Remote monorepo installation: clone to temp, copy subdirectory
|
|
813
851
|
return await installFromMonorepo(parsed, packageName, version);
|
|
@@ -817,6 +855,133 @@ async function installPackage(
|
|
|
817
855
|
}
|
|
818
856
|
}
|
|
819
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Install from a direct file URL (e.g. a single theme JSON hosted on a CDN
|
|
860
|
+
* or GitHub user-attachments). Downloads the file with curl, validates it
|
|
861
|
+
* looks like a Fresh package (currently themes only), and writes it into
|
|
862
|
+
* the appropriate packages directory with a synthetic package.json manifest.
|
|
863
|
+
*
|
|
864
|
+
* Supported file types:
|
|
865
|
+
* - `.json` / `.jsonc` containing a Fresh theme (object with a `name` field
|
|
866
|
+
* and at least one theme section like `editor`, `ui`, `syntax`, etc.)
|
|
867
|
+
*/
|
|
868
|
+
async function installFromDirectFile(
|
|
869
|
+
url: string,
|
|
870
|
+
packageName: string
|
|
871
|
+
): Promise<boolean> {
|
|
872
|
+
const tempFile = editor.pathJoin(
|
|
873
|
+
editor.getTempDir(),
|
|
874
|
+
`fresh-pkg-file-${hashString(url)}-${Date.now()}.json`
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
editor.setStatus(`Downloading ${url}...`);
|
|
878
|
+
const result = await editor.httpFetch(url, tempFile);
|
|
879
|
+
|
|
880
|
+
if (result.exit_code !== 0) {
|
|
881
|
+
// exit_code mirrors editor.httpFetch's contract: 0 on 2xx, the HTTP
|
|
882
|
+
// status code on non-2xx, -1 on transport errors.
|
|
883
|
+
const errorMsg = result.exit_code === 404
|
|
884
|
+
? "File not found"
|
|
885
|
+
: result.exit_code > 0
|
|
886
|
+
? `HTTP ${result.exit_code}`
|
|
887
|
+
: result.stderr.split("\n")[0] || "Download failed";
|
|
888
|
+
editor.setStatus(`Failed to download ${packageName}: ${errorMsg}`);
|
|
889
|
+
editor.removePath(tempFile);
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const content = editor.readFile(tempFile);
|
|
894
|
+
if (!content) {
|
|
895
|
+
editor.setStatus(`Failed to read downloaded file`);
|
|
896
|
+
editor.removePath(tempFile);
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
let parsed: Record<string, unknown>;
|
|
901
|
+
try {
|
|
902
|
+
parsed = JSON.parse(content) as Record<string, unknown>;
|
|
903
|
+
} catch (e) {
|
|
904
|
+
editor.setStatus(`Downloaded file is not valid JSON: ${e}`);
|
|
905
|
+
editor.removePath(tempFile);
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Heuristic: a Fresh theme JSON has a `name` string and at least one of
|
|
910
|
+
// the recognized theme sections. This rules out arbitrary JSON files
|
|
911
|
+
// (configs, package manifests, etc.) being installed as themes.
|
|
912
|
+
const themeName = typeof parsed.name === "string" ? parsed.name : null;
|
|
913
|
+
const looksLikeTheme = themeName !== null && (
|
|
914
|
+
parsed.editor !== undefined ||
|
|
915
|
+
parsed.ui !== undefined ||
|
|
916
|
+
parsed.syntax !== undefined ||
|
|
917
|
+
parsed.search !== undefined ||
|
|
918
|
+
parsed.diagnostics !== undefined ||
|
|
919
|
+
parsed.base !== undefined
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
if (!looksLikeTheme) {
|
|
923
|
+
editor.setStatus(
|
|
924
|
+
`Unrecognized file format at ${url} - direct file install currently supports Fresh theme JSON only`
|
|
925
|
+
);
|
|
926
|
+
editor.removePath(tempFile);
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Use the theme's own name as the package name when available.
|
|
931
|
+
if (themeName) packageName = themeName;
|
|
932
|
+
|
|
933
|
+
// Sanitize for use as a directory name.
|
|
934
|
+
const safeName = packageName.replace(/[^a-zA-Z0-9_.-]/g, "-");
|
|
935
|
+
const targetDir = editor.pathJoin(THEMES_PACKAGES_DIR, safeName);
|
|
936
|
+
|
|
937
|
+
if (editor.fileExists(targetDir)) {
|
|
938
|
+
editor.setStatus(`Package '${safeName}' is already installed`);
|
|
939
|
+
editor.removePath(tempFile);
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
ensureDir(THEMES_PACKAGES_DIR);
|
|
944
|
+
if (!ensureDir(targetDir)) {
|
|
945
|
+
editor.setStatus(`Failed to create package directory ${targetDir}`);
|
|
946
|
+
editor.removePath(tempFile);
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const themeFileName = "theme.json";
|
|
951
|
+
if (!editor.writeFile(editor.pathJoin(targetDir, themeFileName), content)) {
|
|
952
|
+
editor.setStatus(`Failed to write theme file`);
|
|
953
|
+
editor.removePath(tempFile);
|
|
954
|
+
editor.removePath(targetDir);
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const manifest: PackageManifest = {
|
|
959
|
+
name: safeName,
|
|
960
|
+
version: "1.0.0",
|
|
961
|
+
description: `Theme installed from ${url}`,
|
|
962
|
+
type: "theme",
|
|
963
|
+
fresh: {
|
|
964
|
+
themes: [{ file: themeFileName, name: themeName ?? safeName }],
|
|
965
|
+
},
|
|
966
|
+
};
|
|
967
|
+
if (!await writeJsonFile(editor.pathJoin(targetDir, "package.json"), manifest)) {
|
|
968
|
+
editor.setStatus(`Failed to write package manifest`);
|
|
969
|
+
editor.removePath(tempFile);
|
|
970
|
+
editor.removePath(targetDir);
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
await writeJsonFile(editor.pathJoin(targetDir, ".fresh-source.json"), {
|
|
975
|
+
url,
|
|
976
|
+
installed_at: new Date().toISOString(),
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
editor.removePath(tempFile);
|
|
980
|
+
editor.reloadThemes();
|
|
981
|
+
editor.setStatus(`Installed theme ${themeName ?? safeName}`);
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
|
|
820
985
|
/**
|
|
821
986
|
* Install from a standard git repository (no subpath)
|
|
822
987
|
* Clones to temp first to detect type, then moves to correct location.
|
|
@@ -2748,7 +2913,7 @@ async function pkg_install_theme() : Promise<void> {
|
|
|
2748
2913
|
registerHandler("pkg_install_theme", pkg_install_theme);
|
|
2749
2914
|
|
|
2750
2915
|
/**
|
|
2751
|
-
* Install from git URL or local path
|
|
2916
|
+
* Install from git URL, direct file URL, or local path
|
|
2752
2917
|
*/
|
|
2753
2918
|
function pkg_install_url() : void {
|
|
2754
2919
|
editor.startPrompt("Git URL or local path:", "pkg-install-url");
|
|
@@ -73,6 +73,9 @@
|
|
|
73
73
|
38,
|
|
74
74
|
30
|
|
75
75
|
],
|
|
76
|
+
"diff_add_collision_fg": null,
|
|
77
|
+
"diff_remove_collision_fg": null,
|
|
78
|
+
"diff_modify_collision_fg": null,
|
|
76
79
|
"ruler_bg": [
|
|
77
80
|
50,
|
|
78
81
|
50,
|
|
@@ -202,17 +205,17 @@
|
|
|
202
205
|
79,
|
|
203
206
|
120
|
|
204
207
|
],
|
|
208
|
+
"text_input_selection_bg": [
|
|
209
|
+
58,
|
|
210
|
+
79,
|
|
211
|
+
120
|
|
212
|
+
],
|
|
205
213
|
"popup_selection_fg": [
|
|
206
214
|
255,
|
|
207
215
|
255,
|
|
208
216
|
255
|
|
209
217
|
],
|
|
210
218
|
"popup_text_fg": "White",
|
|
211
|
-
"text_input_selection_bg": [
|
|
212
|
-
58,
|
|
213
|
-
79,
|
|
214
|
-
120
|
|
215
|
-
],
|
|
216
219
|
"suggestion_bg": [
|
|
217
220
|
30,
|
|
218
221
|
30,
|
|
@@ -577,6 +580,42 @@
|
|
|
577
580
|
30
|
|
578
581
|
]
|
|
579
582
|
},
|
|
583
|
+
"diff_add_collision_fg": {
|
|
584
|
+
"description": "Fallback fg for cells whose existing fg matches `diff_add_bg`\n(e.g. ANSI Green-on-Green). Only applied on collision; other\ntokens keep their syntax colour.",
|
|
585
|
+
"anyOf": [
|
|
586
|
+
{
|
|
587
|
+
"$ref": "#/$defs/ColorDef"
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
"type": "null"
|
|
591
|
+
}
|
|
592
|
+
],
|
|
593
|
+
"default": null
|
|
594
|
+
},
|
|
595
|
+
"diff_remove_collision_fg": {
|
|
596
|
+
"description": "Collision-only fallback fg for `diff_remove_bg`.",
|
|
597
|
+
"anyOf": [
|
|
598
|
+
{
|
|
599
|
+
"$ref": "#/$defs/ColorDef"
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
"type": "null"
|
|
603
|
+
}
|
|
604
|
+
],
|
|
605
|
+
"default": null
|
|
606
|
+
},
|
|
607
|
+
"diff_modify_collision_fg": {
|
|
608
|
+
"description": "Collision-only fallback fg for `diff_modify_bg`.",
|
|
609
|
+
"anyOf": [
|
|
610
|
+
{
|
|
611
|
+
"$ref": "#/$defs/ColorDef"
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"type": "null"
|
|
615
|
+
}
|
|
616
|
+
],
|
|
617
|
+
"default": null
|
|
618
|
+
},
|
|
580
619
|
"ruler_bg": {
|
|
581
620
|
"description": "Vertical ruler background color",
|
|
582
621
|
"$ref": "#/$defs/ColorDef",
|
|
@@ -953,6 +992,15 @@
|
|
|
953
992
|
120
|
|
954
993
|
]
|
|
955
994
|
},
|
|
995
|
+
"text_input_selection_bg": {
|
|
996
|
+
"description": "Selection background inside a widget Text input. Reads\nagainst `prompt_bg`, so it needs higher contrast against\nthat tint than `editor.selection_bg` (which targets the\neditor surface). Defaults to the same `popup_selection_bg`\nblue used everywhere \"selected item inside a chrome\nsurface\" is shown — same key the prompt selection uses, so\nthe cue reads consistently across selection UIs.",
|
|
997
|
+
"$ref": "#/$defs/ColorDef",
|
|
998
|
+
"default": [
|
|
999
|
+
58,
|
|
1000
|
+
79,
|
|
1001
|
+
120
|
|
1002
|
+
]
|
|
1003
|
+
},
|
|
956
1004
|
"popup_selection_fg": {
|
|
957
1005
|
"description": "Popup selected item text color",
|
|
958
1006
|
"$ref": "#/$defs/ColorDef",
|
|
@@ -967,15 +1015,6 @@
|
|
|
967
1015
|
"$ref": "#/$defs/ColorDef",
|
|
968
1016
|
"default": "White"
|
|
969
1017
|
},
|
|
970
|
-
"text_input_selection_bg": {
|
|
971
|
-
"description": "Selection background inside a widget Text input. Reads against prompt_bg, so it needs higher contrast against that tint than editor.selection_bg.",
|
|
972
|
-
"$ref": "#/$defs/ColorDef",
|
|
973
|
-
"default": [
|
|
974
|
-
58,
|
|
975
|
-
79,
|
|
976
|
-
120
|
|
977
|
-
]
|
|
978
|
-
},
|
|
979
1018
|
"suggestion_bg": {
|
|
980
1019
|
"description": "Autocomplete suggestion background",
|
|
981
1020
|
"$ref": "#/$defs/ColorDef",
|