@fuzdev/fuz_util 0.42.0

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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/array.d.ts +15 -0
  4. package/dist/array.d.ts.map +1 -0
  5. package/dist/array.js +25 -0
  6. package/dist/async.d.ts +62 -0
  7. package/dist/async.d.ts.map +1 -0
  8. package/dist/async.js +147 -0
  9. package/dist/colors.d.ts +41 -0
  10. package/dist/colors.d.ts.map +1 -0
  11. package/dist/colors.js +106 -0
  12. package/dist/counter.d.ts +7 -0
  13. package/dist/counter.d.ts.map +1 -0
  14. package/dist/counter.js +7 -0
  15. package/dist/deep_equal.d.ts +18 -0
  16. package/dist/deep_equal.d.ts.map +1 -0
  17. package/dist/deep_equal.js +152 -0
  18. package/dist/dom.d.ts +35 -0
  19. package/dist/dom.d.ts.map +1 -0
  20. package/dist/dom.js +95 -0
  21. package/dist/error.d.ts +15 -0
  22. package/dist/error.d.ts.map +1 -0
  23. package/dist/error.js +18 -0
  24. package/dist/fetch.d.ts +81 -0
  25. package/dist/fetch.d.ts.map +1 -0
  26. package/dist/fetch.js +162 -0
  27. package/dist/fs.d.ts +34 -0
  28. package/dist/fs.d.ts.map +1 -0
  29. package/dist/fs.js +73 -0
  30. package/dist/function.d.ts +27 -0
  31. package/dist/function.d.ts.map +1 -0
  32. package/dist/function.js +21 -0
  33. package/dist/git.d.ts +132 -0
  34. package/dist/git.d.ts.map +1 -0
  35. package/dist/git.js +288 -0
  36. package/dist/id.d.ts +18 -0
  37. package/dist/id.d.ts.map +1 -0
  38. package/dist/id.js +18 -0
  39. package/dist/iterator.d.ts +5 -0
  40. package/dist/iterator.d.ts.map +1 -0
  41. package/dist/iterator.js +9 -0
  42. package/dist/json.d.ts +30 -0
  43. package/dist/json.d.ts.map +1 -0
  44. package/dist/json.js +44 -0
  45. package/dist/library_json.d.ts +42 -0
  46. package/dist/library_json.d.ts.map +1 -0
  47. package/dist/library_json.js +76 -0
  48. package/dist/log.d.ts +188 -0
  49. package/dist/log.d.ts.map +1 -0
  50. package/dist/log.js +393 -0
  51. package/dist/map.d.ts +12 -0
  52. package/dist/map.d.ts.map +1 -0
  53. package/dist/map.js +14 -0
  54. package/dist/maths.d.ts +85 -0
  55. package/dist/maths.d.ts.map +1 -0
  56. package/dist/maths.js +87 -0
  57. package/dist/object.d.ts +46 -0
  58. package/dist/object.d.ts.map +1 -0
  59. package/dist/object.js +89 -0
  60. package/dist/package_json.d.ts +90 -0
  61. package/dist/package_json.d.ts.map +1 -0
  62. package/dist/package_json.js +112 -0
  63. package/dist/path.d.ts +63 -0
  64. package/dist/path.d.ts.map +1 -0
  65. package/dist/path.js +83 -0
  66. package/dist/print.d.ts +52 -0
  67. package/dist/print.d.ts.map +1 -0
  68. package/dist/print.js +89 -0
  69. package/dist/process.d.ts +77 -0
  70. package/dist/process.d.ts.map +1 -0
  71. package/dist/process.js +148 -0
  72. package/dist/random.d.ts +25 -0
  73. package/dist/random.d.ts.map +1 -0
  74. package/dist/random.js +35 -0
  75. package/dist/random_alea.d.ts +23 -0
  76. package/dist/random_alea.d.ts.map +1 -0
  77. package/dist/random_alea.js +95 -0
  78. package/dist/regexp.d.ts +12 -0
  79. package/dist/regexp.d.ts.map +1 -0
  80. package/dist/regexp.js +16 -0
  81. package/dist/result.d.ts +64 -0
  82. package/dist/result.d.ts.map +1 -0
  83. package/dist/result.js +48 -0
  84. package/dist/source_json.d.ts +375 -0
  85. package/dist/source_json.d.ts.map +1 -0
  86. package/dist/source_json.js +189 -0
  87. package/dist/string.d.ts +51 -0
  88. package/dist/string.d.ts.map +1 -0
  89. package/dist/string.js +92 -0
  90. package/dist/throttle.d.ts +26 -0
  91. package/dist/throttle.d.ts.map +1 -0
  92. package/dist/throttle.js +53 -0
  93. package/dist/timings.d.ts +33 -0
  94. package/dist/timings.d.ts.map +1 -0
  95. package/dist/timings.js +75 -0
  96. package/dist/types.d.ts +77 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +10 -0
  99. package/dist/url.d.ts +10 -0
  100. package/dist/url.d.ts.map +1 -0
  101. package/dist/url.js +8 -0
  102. package/package.json +125 -0
  103. package/src/lib/array.ts +30 -0
  104. package/src/lib/async.ts +182 -0
  105. package/src/lib/colors.ts +132 -0
  106. package/src/lib/counter.ts +11 -0
  107. package/src/lib/deep_equal.ts +155 -0
  108. package/src/lib/dom.ts +108 -0
  109. package/src/lib/error.ts +22 -0
  110. package/src/lib/fetch.ts +231 -0
  111. package/src/lib/fs.ts +128 -0
  112. package/src/lib/function.ts +32 -0
  113. package/src/lib/git.ts +390 -0
  114. package/src/lib/id.ts +30 -0
  115. package/src/lib/iterator.ts +8 -0
  116. package/src/lib/json.ts +61 -0
  117. package/src/lib/library_json.ts +122 -0
  118. package/src/lib/log.ts +469 -0
  119. package/src/lib/map.ts +18 -0
  120. package/src/lib/maths.ts +91 -0
  121. package/src/lib/object.ts +110 -0
  122. package/src/lib/package_json.ts +135 -0
  123. package/src/lib/path.ts +137 -0
  124. package/src/lib/print.ts +111 -0
  125. package/src/lib/process.ts +207 -0
  126. package/src/lib/random.ts +48 -0
  127. package/src/lib/random_alea.ts +107 -0
  128. package/src/lib/regexp.ts +17 -0
  129. package/src/lib/result.ts +67 -0
  130. package/src/lib/source_json.ts +209 -0
  131. package/src/lib/string.ts +99 -0
  132. package/src/lib/throttle.ts +70 -0
  133. package/src/lib/timings.ts +93 -0
  134. package/src/lib/types.ts +99 -0
  135. package/src/lib/url.ts +14 -0
package/dist/fs.js ADDED
@@ -0,0 +1,73 @@
1
+ import { rm, readdir, access, constants } from 'node:fs/promises';
2
+ import { join, isAbsolute } from 'node:path';
3
+ import { EMPTY_OBJECT } from './object.js';
4
+ import { to_array } from './array.js';
5
+ import { ensure_end } from './string.js';
6
+ /**
7
+ * Checks if a file or directory exists.
8
+ */
9
+ export const fs_exists = async (path) => {
10
+ try {
11
+ await access(path, constants.F_OK);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ };
18
+ /**
19
+ * Empties a directory, recursively by default. If `should_remove` is provided, only entries where it returns `true` are removed.
20
+ */
21
+ export const fs_empty_dir = async (dir, should_remove, options) => {
22
+ const entries = await readdir(dir);
23
+ const to_remove = should_remove ? entries.filter(should_remove) : entries;
24
+ await Promise.all(to_remove.map((name) => rm(join(dir, name), { recursive: true, ...options })));
25
+ };
26
+ export const fs_search = async (dir, options = EMPTY_OBJECT) => {
27
+ const { filter, file_filter, sort = default_sort, include_directories = false, cwd = process.cwd(), } = options;
28
+ const final_dir = ensure_end(cwd && !isAbsolute(dir) ? join(cwd, dir) : dir, '/');
29
+ const filters = !filter || (Array.isArray(filter) && !filter.length) ? undefined : to_array(filter);
30
+ const file_filters = !file_filter || (Array.isArray(file_filter) && !file_filter.length)
31
+ ? undefined
32
+ : to_array(file_filter);
33
+ const paths = []; // mutated
34
+ try {
35
+ await crawl(final_dir, paths, filters, file_filters, include_directories, null);
36
+ }
37
+ catch (error) {
38
+ if (error.code === 'ENOENT')
39
+ return [];
40
+ throw error;
41
+ }
42
+ return sort ? paths.sort(typeof sort === 'boolean' ? default_sort : sort) : paths;
43
+ };
44
+ const default_sort = (a, b) => a.path.localeCompare(b.path);
45
+ /**
46
+ * Recursively crawls a directory, collecting paths.
47
+ * @mutates paths - appends discovered files and directories
48
+ */
49
+ const crawl = async (dir, paths, filters, file_filters, include_directories, base_dir) => {
50
+ let subdirs;
51
+ const dirents = await readdir(dir, { withFileTypes: true });
52
+ for (const dirent of dirents) {
53
+ const { name, parentPath } = dirent;
54
+ const is_directory = dirent.isDirectory();
55
+ const id = parentPath + name;
56
+ if (filters && !filters.every((f) => f(id, is_directory))) {
57
+ continue;
58
+ }
59
+ const path = base_dir === null ? name : base_dir + '/' + name;
60
+ if (is_directory) {
61
+ const dir_id = id + '/';
62
+ if (include_directories) {
63
+ paths.push({ path, id: dir_id, is_directory: true });
64
+ }
65
+ (subdirs ??= []).push(crawl(dir_id, paths, filters, file_filters, include_directories, path));
66
+ }
67
+ else if (!file_filters || file_filters.every((f) => f(id))) {
68
+ paths.push({ path, id, is_directory: false });
69
+ }
70
+ }
71
+ if (subdirs)
72
+ await Promise.all(subdirs);
73
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Does nothing when called.
3
+ */
4
+ export declare const noop: (...args: Array<any>) => any;
5
+ /**
6
+ * Async function that returns a resolved promise.
7
+ */
8
+ export declare const noop_async: (...args: Array<any>) => Promise<any>;
9
+ /**
10
+ * A singleton resolved `Promise`.
11
+ */
12
+ export declare const resolved: Promise<void>;
13
+ /**
14
+ * Returns the first arg.
15
+ */
16
+ export declare const identity: <T>(t: T) => T;
17
+ /**
18
+ * Represents a value wrapped in a function.
19
+ * Useful for lazy values and bridging reactive boundaries in Svelte.
20
+ */
21
+ export type Thunk<T> = () => T;
22
+ /**
23
+ * Returns the result of calling `value` if it's a function,
24
+ * otherwise it's like the `identity` function and passes it through.
25
+ */
26
+ export declare const unthunk: <T>(value: T | Thunk<T>) => T;
27
+ //# sourceMappingURL=function.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"function.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/function.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAc,CAAC;AAE3D;;GAEG;AACH,eAAO,MAAM,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,GAAG,CAAkB,CAAC;AAEhF;;GAEG;AACH,eAAO,MAAM,QAAQ,eAAoB,CAAC;AAE1C;;GAEG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,GAAG,CAAC,KAAG,CAAM,CAAC;AAE1C;;;GAGG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;AAE/B;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,KAAG,CACM,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Does nothing when called.
3
+ */
4
+ export const noop = () => { }; // eslint-disable-line @typescript-eslint/no-empty-function
5
+ /**
6
+ * Async function that returns a resolved promise.
7
+ */
8
+ export const noop_async = () => resolved;
9
+ /**
10
+ * A singleton resolved `Promise`.
11
+ */
12
+ export const resolved = Promise.resolve();
13
+ /**
14
+ * Returns the first arg.
15
+ */
16
+ export const identity = (t) => t;
17
+ /**
18
+ * Returns the result of calling `value` if it's a function,
19
+ * otherwise it's like the `identity` function and passes it through.
20
+ */
21
+ export const unthunk = (value) => typeof value === 'function' ? value() : value;
package/dist/git.d.ts ADDED
@@ -0,0 +1,132 @@
1
+ import type { SpawnOptions } from 'node:child_process';
2
+ import { z } from 'zod';
3
+ import type { Flavored } from './types.js';
4
+ export declare const GitOrigin: z.ZodString;
5
+ export type GitOrigin = Flavored<string, 'GitOrigin'>;
6
+ export declare const GitBranch: z.ZodString;
7
+ export type GitBranch = Flavored<string, 'GitBranch'>;
8
+ /**
9
+ * Returns the current git branch name or throws if something goes wrong.
10
+ */
11
+ export declare const git_current_branch_name: (options?: SpawnOptions) => Promise<GitBranch>;
12
+ /**
13
+ * @returns a boolean indicating if the remote git branch exists
14
+ */
15
+ export declare const git_remote_branch_exists: (origin?: GitOrigin, branch?: GitBranch, options?: SpawnOptions) => Promise<boolean>;
16
+ /**
17
+ * @returns a boolean indicating if the local git branch exists
18
+ */
19
+ export declare const git_local_branch_exists: (branch: GitBranch, options?: SpawnOptions) => Promise<boolean>;
20
+ /**
21
+ * Git workspace status flags indicating which types of changes are present.
22
+ */
23
+ export interface GitWorkspaceStatus {
24
+ unstaged_changes: boolean;
25
+ staged_changes: boolean;
26
+ untracked_files: boolean;
27
+ }
28
+ /**
29
+ * Parses the output of `git status --porcelain -z` (v1 format) into a status object.
30
+ * This is a pure function that can be tested independently.
31
+ *
32
+ * Format: XY path\0 where:
33
+ * - X = staged status (index)
34
+ * - Y = unstaged status (work tree)
35
+ * - path = file path (unescaped with -z)
36
+ *
37
+ * Supported status codes:
38
+ * - M = modified
39
+ * - A = added
40
+ * - D = deleted
41
+ * - R = renamed
42
+ * - C = copied
43
+ * - T = type changed (regular file, symbolic link or submodule)
44
+ * - U = unmerged
45
+ * - ? = untracked
46
+ * - ! = ignored
47
+ *
48
+ * For renames/copies: XY new\0old\0 (two NUL-separated paths)
49
+ *
50
+ * Note: This implementation treats submodules the same as regular files.
51
+ * Submodule-specific status codes (lowercase m, ?) are interpreted as changes.
52
+ *
53
+ * @param stdout - The raw output from `git status --porcelain -z`
54
+ * @returns status object with flags for unstaged changes, staged changes, and untracked files
55
+ */
56
+ export declare const git_parse_workspace_status: (stdout: string | null) => GitWorkspaceStatus;
57
+ /**
58
+ * Checks the git workspace status using a single `git status --porcelain -z` call.
59
+ * The -z format provides more reliable parsing by using NUL separators and avoiding escaping.
60
+ * @returns status object with flags for unstaged changes, staged changes, and untracked files
61
+ */
62
+ export declare const git_check_workspace: (options?: SpawnOptions) => Promise<GitWorkspaceStatus>;
63
+ /**
64
+ * @returns `true` if the workspace has no changes at all
65
+ */
66
+ export declare const git_workspace_is_clean: (status: GitWorkspaceStatus) => boolean;
67
+ /**
68
+ * @returns `true` if the workspace has no unstaged changes and no untracked files (staged changes are OK)
69
+ */
70
+ export declare const git_workspace_is_fully_staged: (status: GitWorkspaceStatus) => boolean;
71
+ /**
72
+ * Converts a workspace status to a human-readable message.
73
+ */
74
+ export declare const git_workspace_status_message: (status: GitWorkspaceStatus) => string;
75
+ /**
76
+ * @returns an error message if the git workspace has any unstaged or uncommitted changes, or `null` if it's clean
77
+ */
78
+ export declare const git_check_clean_workspace: (options?: SpawnOptions) => Promise<string | null>;
79
+ /**
80
+ * @returns an error message if the git workspace has any unstaged changes or untracked files, or `null` if fully staged
81
+ */
82
+ export declare const git_check_fully_staged_workspace: (options?: SpawnOptions) => Promise<string | null>;
83
+ /**
84
+ * Calls `git fetch` and throws if anything goes wrong.
85
+ */
86
+ export declare const git_fetch: (origin?: GitOrigin, branch?: GitBranch, options?: SpawnOptions) => Promise<void>;
87
+ /**
88
+ * Calls `git checkout` and throws if anything goes wrong.
89
+ * @returns the previous branch name, if it changed
90
+ */
91
+ export declare const git_checkout: (branch: GitBranch, options?: SpawnOptions) => Promise<GitBranch | null>;
92
+ /**
93
+ * Calls `git pull` and throws if anything goes wrong.
94
+ */
95
+ export declare const git_pull: (origin?: GitOrigin, branch?: GitBranch, options?: SpawnOptions) => Promise<void>;
96
+ /**
97
+ * Calls `git push` and throws if anything goes wrong.
98
+ */
99
+ export declare const git_push: (origin: GitOrigin, branch?: GitBranch, options?: SpawnOptions, set_upstream?: boolean) => Promise<void>;
100
+ /**
101
+ * Calls `git push` and throws if anything goes wrong.
102
+ */
103
+ export declare const git_push_to_create: (origin?: GitOrigin, branch?: GitBranch, options?: SpawnOptions) => Promise<void>;
104
+ /**
105
+ * Deletes a branch locally and throws if anything goes wrong.
106
+ */
107
+ export declare const git_delete_local_branch: (branch: GitBranch, options?: SpawnOptions) => Promise<void>;
108
+ /**
109
+ * Deletes a branch remotely and throws if anything goes wrong.
110
+ */
111
+ export declare const git_delete_remote_branch: (origin: GitOrigin, branch: GitBranch, options?: SpawnOptions) => Promise<void>;
112
+ /**
113
+ * Resets the `target` branch back to its first commit both locally and remotely.
114
+ */
115
+ export declare const git_reset_branch_to_first_commit: (origin: GitOrigin, branch: GitBranch, options?: SpawnOptions) => Promise<void>;
116
+ /**
117
+ * Returns the branch's latest commit hash or throws if something goes wrong.
118
+ */
119
+ export declare const git_current_commit_hash: (branch?: string, options?: SpawnOptions) => Promise<string | null>;
120
+ /**
121
+ * Returns the hash of the current branch's first commit or throws if something goes wrong.
122
+ */
123
+ export declare const git_current_branch_first_commit_hash: (options?: SpawnOptions) => Promise<string>;
124
+ /**
125
+ * Returns the global git config setting for `pull.rebase`.
126
+ */
127
+ export declare const git_check_setting_pull_rebase: (options?: SpawnOptions) => Promise<boolean>;
128
+ /**
129
+ * Clones a branch locally to another directory and updates the origin to match the source.
130
+ */
131
+ export declare const git_clone_locally: (origin: GitOrigin, branch: GitBranch, source_dir: string, target_dir: string, options?: SpawnOptions) => Promise<void>;
132
+ //# sourceMappingURL=git.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/git.ts"],"names":[],"mappings":"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"}
package/dist/git.js ADDED
@@ -0,0 +1,288 @@
1
+ import { z } from 'zod';
2
+ import { spawn, spawn_out } from './process.js';
3
+ import { to_file_path } from './path.js';
4
+ import { fs_exists } from './fs.js';
5
+ export const GitOrigin = z.string();
6
+ export const GitBranch = z.string();
7
+ /**
8
+ * Returns the current git branch name or throws if something goes wrong.
9
+ */
10
+ export const git_current_branch_name = async (options) => {
11
+ const { stdout } = await spawn_out('git', ['rev-parse', '--abbrev-ref', 'HEAD'], options);
12
+ if (!stdout)
13
+ throw Error('git_current_branch_name failed');
14
+ const branch_name = stdout.trim();
15
+ return branch_name;
16
+ };
17
+ /**
18
+ * @returns a boolean indicating if the remote git branch exists
19
+ */
20
+ export const git_remote_branch_exists = async (origin = 'origin', branch, options) => {
21
+ const final_branch = branch ?? (await git_current_branch_name(options));
22
+ if (options?.cwd && !(await fs_exists(to_file_path(options.cwd)))) {
23
+ return false;
24
+ }
25
+ const result = await spawn('git', ['ls-remote', '--exit-code', '--heads', origin, 'refs/heads/' + final_branch], options);
26
+ if (result.ok) {
27
+ return true;
28
+ }
29
+ else if (result.code === 2) {
30
+ return false;
31
+ }
32
+ else {
33
+ throw Error(`git_remote_branch_exists failed for origin '${origin}' and branch '${final_branch}' with code ${result.code}`);
34
+ }
35
+ };
36
+ /**
37
+ * @returns a boolean indicating if the local git branch exists
38
+ */
39
+ export const git_local_branch_exists = async (branch, options) => {
40
+ if (options?.cwd && !(await fs_exists(to_file_path(options.cwd)))) {
41
+ return false;
42
+ }
43
+ const result = await spawn('git', ['show-ref', '--quiet', 'refs/heads/' + branch], options);
44
+ return result.ok;
45
+ };
46
+ /**
47
+ * Parses the output of `git status --porcelain -z` (v1 format) into a status object.
48
+ * This is a pure function that can be tested independently.
49
+ *
50
+ * Format: XY path\0 where:
51
+ * - X = staged status (index)
52
+ * - Y = unstaged status (work tree)
53
+ * - path = file path (unescaped with -z)
54
+ *
55
+ * Supported status codes:
56
+ * - M = modified
57
+ * - A = added
58
+ * - D = deleted
59
+ * - R = renamed
60
+ * - C = copied
61
+ * - T = type changed (regular file, symbolic link or submodule)
62
+ * - U = unmerged
63
+ * - ? = untracked
64
+ * - ! = ignored
65
+ *
66
+ * For renames/copies: XY new\0old\0 (two NUL-separated paths)
67
+ *
68
+ * Note: This implementation treats submodules the same as regular files.
69
+ * Submodule-specific status codes (lowercase m, ?) are interpreted as changes.
70
+ *
71
+ * @param stdout - The raw output from `git status --porcelain -z`
72
+ * @returns status object with flags for unstaged changes, staged changes, and untracked files
73
+ */
74
+ export const git_parse_workspace_status = (stdout) => {
75
+ // Split on NUL character (\0) for -z format
76
+ // Filter out empty strings (last element after final \0)
77
+ const entries = stdout?.split('\0').filter(Boolean) ?? [];
78
+ // For rename/copy operations, we need to skip the old filename entry
79
+ // Format: R new\0old\0 or C new\0old\0
80
+ let skipNext = false;
81
+ const lines = [];
82
+ for (const entry of entries) {
83
+ if (skipNext) {
84
+ skipNext = false;
85
+ continue;
86
+ }
87
+ // Check if this is a rename/copy operation in either position
88
+ // R = renamed in index, RM = renamed in index + modified in work tree
89
+ // R = renamed in work tree (rare but possible with certain configs)
90
+ if (entry.length >= 3 &&
91
+ (entry[0] === 'R' || entry[0] === 'C' || entry[1] === 'R' || entry[1] === 'C')) {
92
+ skipNext = true;
93
+ }
94
+ lines.push(entry);
95
+ }
96
+ return {
97
+ // Y position (index 1) - any non-space, non-?, non-! means unstaged changes
98
+ // Minimum length is 3: XY followed by at least one space (e.g., "M ")
99
+ unstaged_changes: lines.some((line) => line.length >= 3 && line[1] !== ' ' && line[1] !== '?' && line[1] !== '!'),
100
+ // X position (index 0) - any non-space, non-?, non-! means staged changes
101
+ // Minimum length is 3: XY followed by at least one space (e.g., "M ")
102
+ staged_changes: lines.some((line) => line.length >= 3 && line[0] !== ' ' && line[0] !== '?' && line[0] !== '!'),
103
+ // ?? prefix means untracked files
104
+ untracked_files: lines.some((line) => line.startsWith('??')),
105
+ };
106
+ };
107
+ /**
108
+ * Checks the git workspace status using a single `git status --porcelain -z` call.
109
+ * The -z format provides more reliable parsing by using NUL separators and avoiding escaping.
110
+ * @returns status object with flags for unstaged changes, staged changes, and untracked files
111
+ */
112
+ export const git_check_workspace = async (options) => {
113
+ const { stdout } = await spawn_out('git', ['status', '--porcelain', '-z'], options);
114
+ return git_parse_workspace_status(stdout);
115
+ };
116
+ /**
117
+ * @returns `true` if the workspace has no changes at all
118
+ */
119
+ export const git_workspace_is_clean = (status) => !status.unstaged_changes && !status.staged_changes && !status.untracked_files;
120
+ /**
121
+ * @returns `true` if the workspace has no unstaged changes and no untracked files (staged changes are OK)
122
+ */
123
+ export const git_workspace_is_fully_staged = (status) => !status.unstaged_changes && !status.untracked_files;
124
+ /**
125
+ * Converts a workspace status to a human-readable message.
126
+ */
127
+ export const git_workspace_status_message = (status) => {
128
+ if (git_workspace_is_clean(status))
129
+ return 'workspace is clean';
130
+ const issues = [];
131
+ if (status.unstaged_changes)
132
+ issues.push('unstaged changes');
133
+ if (status.staged_changes)
134
+ issues.push('staged but uncommitted changes');
135
+ if (status.untracked_files)
136
+ issues.push('untracked files');
137
+ return `git has ${issues.join(', ')}`;
138
+ };
139
+ /**
140
+ * @returns an error message if the git workspace has any unstaged or uncommitted changes, or `null` if it's clean
141
+ */
142
+ export const git_check_clean_workspace = async (options) => {
143
+ const status = await git_check_workspace(options);
144
+ return git_workspace_is_clean(status) ? null : git_workspace_status_message(status);
145
+ };
146
+ /**
147
+ * @returns an error message if the git workspace has any unstaged changes or untracked files, or `null` if fully staged
148
+ */
149
+ export const git_check_fully_staged_workspace = async (options) => {
150
+ const status = await git_check_workspace(options);
151
+ return git_workspace_is_fully_staged(status) ? null : git_workspace_status_message(status);
152
+ };
153
+ /**
154
+ * Calls `git fetch` and throws if anything goes wrong.
155
+ */
156
+ export const git_fetch = async (origin = 'origin', branch, options) => {
157
+ const args = ['fetch', origin];
158
+ if (branch)
159
+ args.push(branch);
160
+ const result = await spawn('git', args, options);
161
+ if (!result.ok) {
162
+ throw Error(`git_fetch failed for origin '${origin}' and branch '${branch}' with code ${result.code}`);
163
+ }
164
+ };
165
+ /**
166
+ * Calls `git checkout` and throws if anything goes wrong.
167
+ * @returns the previous branch name, if it changed
168
+ */
169
+ export const git_checkout = async (branch, options) => {
170
+ const current_branch = await git_current_branch_name(options);
171
+ if (branch === current_branch) {
172
+ return null;
173
+ }
174
+ const result = await spawn('git', ['checkout', branch], options);
175
+ if (!result.ok) {
176
+ throw Error(`git_checkout failed for branch '${branch}' with code ${result.code}`);
177
+ }
178
+ return current_branch;
179
+ };
180
+ /**
181
+ * Calls `git pull` and throws if anything goes wrong.
182
+ */
183
+ export const git_pull = async (origin = 'origin', branch, options) => {
184
+ const args = ['pull', origin];
185
+ if (branch)
186
+ args.push(branch);
187
+ const result = await spawn('git', args, options);
188
+ if (!result.ok) {
189
+ throw Error(`git_pull failed for branch '${branch}' with code ${result.code}`);
190
+ }
191
+ };
192
+ /**
193
+ * Calls `git push` and throws if anything goes wrong.
194
+ */
195
+ export const git_push = async (origin, branch, options, set_upstream = false) => {
196
+ const final_branch = branch ?? (await git_current_branch_name(options));
197
+ const args = ['push', origin, final_branch];
198
+ if (set_upstream)
199
+ args.push('-u');
200
+ const result = await spawn('git', args, options);
201
+ if (!result.ok) {
202
+ throw Error(`git_push failed for branch '${final_branch}' with code ${result.code}`);
203
+ }
204
+ };
205
+ /**
206
+ * Calls `git push` and throws if anything goes wrong.
207
+ */
208
+ export const git_push_to_create = async (origin = 'origin', branch, options) => {
209
+ const final_branch = branch ?? (await git_current_branch_name(options));
210
+ const push_args = ['push'];
211
+ if (await git_remote_branch_exists(origin, final_branch, options)) {
212
+ push_args.push(origin);
213
+ }
214
+ else {
215
+ push_args.push('-u', origin);
216
+ }
217
+ push_args.push(final_branch);
218
+ const result = await spawn('git', push_args, options);
219
+ if (!result.ok) {
220
+ throw Error(`git_push failed for branch '${final_branch}' with code ${result.code}`);
221
+ }
222
+ };
223
+ /**
224
+ * Deletes a branch locally and throws if anything goes wrong.
225
+ */
226
+ export const git_delete_local_branch = async (branch, options) => {
227
+ const result = await spawn('git', ['branch', '-D', branch], options);
228
+ if (!result.ok) {
229
+ throw Error(`git_delete_local_branch failed for branch '${branch}' with code ${result.code}`);
230
+ }
231
+ };
232
+ /**
233
+ * Deletes a branch remotely and throws if anything goes wrong.
234
+ */
235
+ export const git_delete_remote_branch = async (origin, branch, options) => {
236
+ const result = await spawn('git', ['push', origin, ':' + branch], options);
237
+ if (!result.ok) {
238
+ throw Error(`git_delete_remote_branch failed for branch '${branch}' with code ${result.code}`);
239
+ }
240
+ };
241
+ /**
242
+ * Resets the `target` branch back to its first commit both locally and remotely.
243
+ */
244
+ export const git_reset_branch_to_first_commit = async (origin, branch, options) => {
245
+ const previous_branch = await git_checkout(branch, options);
246
+ const first_commit_hash = await git_current_branch_first_commit_hash(options);
247
+ await spawn('git', ['reset', '--hard', first_commit_hash], options);
248
+ await spawn('git', ['push', origin, branch, '--force'], options);
249
+ if (previous_branch) {
250
+ await git_checkout(previous_branch, options);
251
+ }
252
+ };
253
+ /**
254
+ * Returns the branch's latest commit hash or throws if something goes wrong.
255
+ */
256
+ export const git_current_commit_hash = async (branch, options) => {
257
+ const final_branch = branch ?? (await git_current_branch_name(options));
258
+ const { stdout } = await spawn_out('git', ['show-ref', '-s', final_branch], options);
259
+ if (!stdout)
260
+ return null; // TODO hack for CI
261
+ return stdout.split('\n')[0].trim();
262
+ };
263
+ /**
264
+ * Returns the hash of the current branch's first commit or throws if something goes wrong.
265
+ */
266
+ export const git_current_branch_first_commit_hash = async (options) => {
267
+ const { stdout } = await spawn_out('git', ['rev-list', '--max-parents=0', '--abbrev-commit', 'HEAD'], options);
268
+ if (!stdout)
269
+ throw Error('git_current_branch_first_commit_hash failed');
270
+ return stdout.trim();
271
+ };
272
+ /**
273
+ * Returns the global git config setting for `pull.rebase`.
274
+ */
275
+ export const git_check_setting_pull_rebase = async (options) => {
276
+ const value = await spawn_out('git', ['config', '--global', 'pull.rebase'], options);
277
+ return value.stdout?.trim() === 'true';
278
+ };
279
+ /**
280
+ * Clones a branch locally to another directory and updates the origin to match the source.
281
+ */
282
+ export const git_clone_locally = async (origin, branch, source_dir, target_dir, options) => {
283
+ await spawn('git', ['clone', '-b', branch, '--single-branch', source_dir, target_dir], options);
284
+ const origin_url = (await spawn_out('git', ['remote', 'get-url', origin], { ...options, cwd: source_dir })).stdout?.trim();
285
+ if (!origin_url)
286
+ throw Error('Failed to get the origin url with git in ' + source_dir);
287
+ await spawn('git', ['remote', 'set-url', origin, origin_url], { ...options, cwd: target_dir });
288
+ };
package/dist/id.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { Flavored } from './types.js';
2
+ export type Uuid = Flavored<string, 'Uuid'>;
3
+ /**
4
+ * Loosely validates a UUID string.
5
+ */
6
+ export declare const is_uuid: (str: string) => str is Uuid;
7
+ /**
8
+ * Postgres doesn't support the namespace prefix, so neither does Felt.
9
+ * For more see the UUID RFC - https://tools.ietf.org/html/rfc4122
10
+ * The Ajv validator does support the namespace, hence this custom implementation.
11
+ */
12
+ export declare const UUID_MATCHER: RegExp;
13
+ export type ClientIdCreator = () => string;
14
+ /**
15
+ * Creates a string id generator function, outputting `${name}_${count}` by default.
16
+ */
17
+ export declare const create_client_id_creator: (name: string, count?: number, separator?: string) => ClientIdCreator;
18
+ //# sourceMappingURL=id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/id.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AAGzC,MAAM,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5C;;GAEG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,IAA8B,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAoD,CAAC;AAE9E,MAAM,MAAM,eAAe,GAAG,MAAM,MAAM,CAAC;AAE3C;;GAEG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,kBAAe,KACb,eAGF,CAAC"}
package/dist/id.js ADDED
@@ -0,0 +1,18 @@
1
+ import { create_counter } from './counter.js';
2
+ /**
3
+ * Loosely validates a UUID string.
4
+ */
5
+ export const is_uuid = (str) => UUID_MATCHER.test(str);
6
+ /**
7
+ * Postgres doesn't support the namespace prefix, so neither does Felt.
8
+ * For more see the UUID RFC - https://tools.ietf.org/html/rfc4122
9
+ * The Ajv validator does support the namespace, hence this custom implementation.
10
+ */
11
+ export const UUID_MATCHER = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu;
12
+ /**
13
+ * Creates a string id generator function, outputting `${name}_${count}` by default.
14
+ */
15
+ export const create_client_id_creator = (name, count, separator = '_') => {
16
+ const counter = create_counter(count);
17
+ return () => `${name}${separator}${counter()}`;
18
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Useful for fast counting when `.length` is not available.
3
+ */
4
+ export declare const count_iterator: (iterator: Iterable<unknown>) => number;
5
+ //# sourceMappingURL=iterator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iterator.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/iterator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,cAAc,GAAI,UAAU,QAAQ,CAAC,OAAO,CAAC,KAAG,MAI5D,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Useful for fast counting when `.length` is not available.
3
+ */
4
+ export const count_iterator = (iterator) => {
5
+ var count = 0;
6
+ for (var _ of iterator)
7
+ count++;
8
+ return count;
9
+ };
package/dist/json.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ export type Json = JsonPrimitive | JsonObject | JsonArray;
2
+ export type JsonPrimitive = string | number | boolean | null;
3
+ export interface JsonObject extends Record<string, Json> {
4
+ }
5
+ export interface JsonArray extends Array<Json> {
6
+ }
7
+ /**
8
+ * Like `typeof json`, but includes arrays. Excludes `'undefined'` because it's not valid JSON.
9
+ */
10
+ export type JsonType = 'string' | 'number' | 'boolean' | 'null' | 'object' | 'array';
11
+ /**
12
+ * Returns the `JsonType` of `value`, which is like `typeof json`
13
+ * but includes `'array'` and omits `'undefined'`.
14
+ */
15
+ export declare const json_type_of: (value: Json) => JsonType | undefined;
16
+ /**
17
+ * Embeds `data` as a JSON string, escaping single quotes.
18
+ * Useful for optimizing JSON in JS because it parses faster.
19
+ */
20
+ export declare const json_embed: <T>(data: T, stringify?: (data: T) => string) => string;
21
+ /**
22
+ * Serializes a value to JSON with deterministic key ordering.
23
+ * Recursively sorts object keys alphabetically for consistent hashing.
24
+ * Arrays and primitives are serialized as-is.
25
+ *
26
+ * @param value Any JSON-serializable value
27
+ * @returns Deterministic JSON string representation
28
+ */
29
+ export declare const json_stringify_deterministic: (value: unknown) => string;
30
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/json.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,aAAa,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAE7D,MAAM,WAAW,UAAW,SAAQ,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC;CAAG;AAE3D,MAAM,WAAW,SAAU,SAAQ,KAAK,CAAC,IAAI,CAAC;CAAG;AAEjD;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAErF;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,IAAI,KAAG,QAAQ,GAAG,SAerD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,MAAM,CAAC,EAAE,YAAW,CAAC,IAAI,EAAE,CAAC,KAAK,MAAuB,KAAG,MACN,CAAC;AAEpF;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,GAAI,OAAO,OAAO,KAAG,MAW3D,CAAC"}