isomorphic-jj 1.0.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.
- package/LICENSE +21 -0
- package/README.md +971 -0
- package/dist/index.cjs +12609 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +12600 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +90 -0
- package/src/api/repository.js +6885 -0
- package/src/backend/isomorphic-git-backend.js +687 -0
- package/src/backend/lazy-git-backend.js +180 -0
- package/src/browser/helpers.js +189 -0
- package/src/browser/index.d.ts +174 -0
- package/src/browser/index.js +12 -0
- package/src/core/background-ops.js +272 -0
- package/src/core/bookmark-store.js +232 -0
- package/src/core/change-graph.js +285 -0
- package/src/core/conflict-model.js +505 -0
- package/src/core/jj-checkout.js +78 -0
- package/src/core/jj-operation-store.js +120 -0
- package/src/core/jj-tree-state.js +94 -0
- package/src/core/jj-view-store.js +105 -0
- package/src/core/merge-driver-registry.js +432 -0
- package/src/core/operation-log.js +272 -0
- package/src/core/revset-engine.js +1081 -0
- package/src/core/storage-manager.js +207 -0
- package/src/core/tag-store.js +167 -0
- package/src/core/user-config.js +142 -0
- package/src/core/working-copy.js +314 -0
- package/src/core/workspace-manager.js +283 -0
- package/src/drivers/built-in-drivers.js +296 -0
- package/src/index.js +15 -0
- package/src/protos/local_working_copy.proto +76 -0
- package/src/protos/simple_op_store.proto +147 -0
- package/src/types.d.ts +1129 -0
- package/src/utils/errors.js +21 -0
- package/src/utils/id-generation.js +54 -0
- package/src/utils/validation.js +124 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Manager for isomorphic-jj
|
|
3
|
+
*
|
|
4
|
+
* Manages JSON storage in .jj directory with atomic writes and caching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { JJError } from '../utils/errors.js';
|
|
8
|
+
|
|
9
|
+
export class Storage {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} fs - Filesystem implementation (Node fs, LightningFS, etc.)
|
|
12
|
+
* @param {string} dir - Repository directory path
|
|
13
|
+
*/
|
|
14
|
+
constructor(fs, dir) {
|
|
15
|
+
this.fs = fs;
|
|
16
|
+
this.dir = dir;
|
|
17
|
+
this.jjDir = `${dir}/.jj`;
|
|
18
|
+
this.repoDir = `${dir}/.jj/repo`; // Core repo data
|
|
19
|
+
this.workingCopyDir = `${dir}/.jj/working_copy`; // Default workspace working copy
|
|
20
|
+
this.cache = new Map();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize .jj directory structure (JJ workspace model)
|
|
25
|
+
*
|
|
26
|
+
* Structure:
|
|
27
|
+
* .jj/
|
|
28
|
+
* repo/ - Core repository data (shared by all workspaces)
|
|
29
|
+
* store/ - Change graph, operations, bookmarks
|
|
30
|
+
* working_copy/ - Default workspace working copy state
|
|
31
|
+
*/
|
|
32
|
+
async init() {
|
|
33
|
+
try {
|
|
34
|
+
// Create main directories
|
|
35
|
+
await this.fs.promises.mkdir(this.jjDir, { recursive: true });
|
|
36
|
+
await this.fs.promises.mkdir(this.repoDir, { recursive: true });
|
|
37
|
+
await this.fs.promises.mkdir(this.workingCopyDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
// Create repo subdirectories
|
|
40
|
+
await this.fs.promises.mkdir(`${this.repoDir}/store`, { recursive: true });
|
|
41
|
+
await this.fs.promises.mkdir(`${this.repoDir}/op_log`, { recursive: true });
|
|
42
|
+
await this.fs.promises.mkdir(`${this.repoDir}/conflicts`, { recursive: true });
|
|
43
|
+
|
|
44
|
+
// Create default workspace directory
|
|
45
|
+
await this.fs.promises.mkdir(`${this.workingCopyDir}/default`, { recursive: true });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new JJError('STORAGE_INIT_FAILED', `Failed to initialize .jj directory: ${error.message}`, {
|
|
48
|
+
dir: this.jjDir,
|
|
49
|
+
originalError: error,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read and parse JSON file
|
|
56
|
+
*
|
|
57
|
+
* @param {string} path - Relative path from .jj directory
|
|
58
|
+
* @returns {Promise<Object|null>} Parsed JSON or null if file doesn't exist
|
|
59
|
+
*/
|
|
60
|
+
async read(path) {
|
|
61
|
+
const fullPath = `${this.jjDir}/${path}`;
|
|
62
|
+
|
|
63
|
+
// Check cache
|
|
64
|
+
if (this.cache.has(path)) {
|
|
65
|
+
return this.cache.get(path);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const content = await this.fs.promises.readFile(fullPath, 'utf8');
|
|
70
|
+
const data = JSON.parse(content);
|
|
71
|
+
this.cache.set(path, data);
|
|
72
|
+
return data;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code === 'ENOENT') {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
throw new JJError('STORAGE_READ_FAILED', `Failed to read ${path}: ${error.message}`, {
|
|
78
|
+
path: fullPath,
|
|
79
|
+
originalError: error,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Write JSON file atomically
|
|
86
|
+
*
|
|
87
|
+
* @param {string} path - Relative path from .jj directory
|
|
88
|
+
* @param {Object|string} data - Data to write (object will be stringified)
|
|
89
|
+
*/
|
|
90
|
+
async write(path, data) {
|
|
91
|
+
const fullPath = `${this.jjDir}/${path}`;
|
|
92
|
+
const tmpPath = `${fullPath}.tmp.${Date.now()}`;
|
|
93
|
+
const jsonData = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Ensure parent directory exists
|
|
97
|
+
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/'));
|
|
98
|
+
await this.fs.promises.mkdir(dirPath, { recursive: true });
|
|
99
|
+
|
|
100
|
+
// Write to temp file
|
|
101
|
+
await this.fs.promises.writeFile(tmpPath, jsonData, 'utf8');
|
|
102
|
+
|
|
103
|
+
// Atomic rename
|
|
104
|
+
await this.fs.promises.rename(tmpPath, fullPath);
|
|
105
|
+
|
|
106
|
+
// Update cache
|
|
107
|
+
if (typeof data !== 'string') {
|
|
108
|
+
this.cache.set(path, data);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// Clean up temp file
|
|
112
|
+
try {
|
|
113
|
+
await this.fs.promises.unlink(tmpPath);
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
throw new JJError('STORAGE_WRITE_FAILED', `Failed to write ${path}: ${error.message}`, {
|
|
117
|
+
path: fullPath,
|
|
118
|
+
originalError: error,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read JSONL file (newline-delimited JSON)
|
|
125
|
+
*
|
|
126
|
+
* @param {string} path - Relative path from .jj directory
|
|
127
|
+
* @returns {Promise<Array>} Array of parsed JSON objects
|
|
128
|
+
*/
|
|
129
|
+
async readLines(path) {
|
|
130
|
+
const fullPath = `${this.jjDir}/${path}`;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const content = await this.fs.promises.readFile(fullPath, 'utf8');
|
|
134
|
+
return content
|
|
135
|
+
.trim()
|
|
136
|
+
.split('\n')
|
|
137
|
+
.filter((line) => line.length > 0)
|
|
138
|
+
.map((line) => JSON.parse(line));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error.code === 'ENOENT') {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
throw new JJError('STORAGE_READ_FAILED', `Failed to read ${path}: ${error.message}`, {
|
|
144
|
+
path: fullPath,
|
|
145
|
+
originalError: error,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Append line to JSONL file
|
|
152
|
+
*
|
|
153
|
+
* @param {string} path - Relative path from .jj directory
|
|
154
|
+
* @param {string} line - JSON string to append
|
|
155
|
+
*/
|
|
156
|
+
async appendLine(path, line) {
|
|
157
|
+
const fullPath = `${this.jjDir}/${path}`;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// Read existing content
|
|
161
|
+
let existing = '';
|
|
162
|
+
try {
|
|
163
|
+
existing = await this.fs.promises.readFile(fullPath, 'utf8');
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error.code !== 'ENOENT') throw error;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Write atomically
|
|
169
|
+
await this.write(path, existing + line + '\n');
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new JJError('STORAGE_WRITE_FAILED', `Failed to append to ${path}: ${error.message}`, {
|
|
172
|
+
path: fullPath,
|
|
173
|
+
originalError: error,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if file exists
|
|
180
|
+
*
|
|
181
|
+
* @param {string} path - Relative path from .jj directory
|
|
182
|
+
* @returns {Promise<boolean>}
|
|
183
|
+
*/
|
|
184
|
+
async exists(path) {
|
|
185
|
+
const fullPath = `${this.jjDir}/${path}`;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await this.fs.promises.stat(fullPath);
|
|
189
|
+
return true;
|
|
190
|
+
} catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Invalidate cache
|
|
197
|
+
*
|
|
198
|
+
* @param {string} [path] - Specific path to invalidate, or all if not provided
|
|
199
|
+
*/
|
|
200
|
+
invalidateCache(path) {
|
|
201
|
+
if (path) {
|
|
202
|
+
this.cache.delete(path);
|
|
203
|
+
} else {
|
|
204
|
+
this.cache.clear();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag Store - Manages immutable references to changes
|
|
3
|
+
*
|
|
4
|
+
* Tags are immutable references that cannot be moved once created.
|
|
5
|
+
* They are typically used for release points and important milestones.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { JJError } from '../utils/errors.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates tag name format
|
|
12
|
+
* @param {string} name - Tag name to validate
|
|
13
|
+
* @throws {JJError} If name is invalid
|
|
14
|
+
*/
|
|
15
|
+
function validateTagName(name) {
|
|
16
|
+
if (!name || typeof name !== 'string') {
|
|
17
|
+
throw new JJError('INVALID_TAG_NAME', 'Tag name must be a non-empty string');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (name.trim() !== name || name.includes(' ')) {
|
|
21
|
+
throw new JJError(
|
|
22
|
+
'INVALID_TAG_NAME',
|
|
23
|
+
'Tag name cannot contain spaces or leading/trailing whitespace',
|
|
24
|
+
{ name }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (name.length === 0) {
|
|
29
|
+
throw new JJError('INVALID_TAG_NAME', 'Tag name cannot be empty');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Simple tag pattern matching (supports glob-like patterns)
|
|
35
|
+
* @param {string} name - Tag name
|
|
36
|
+
* @param {string} pattern - Pattern (e.g., "v1*", "*-rc")
|
|
37
|
+
* @returns {boolean} True if name matches pattern
|
|
38
|
+
*/
|
|
39
|
+
function matchesPattern(name, pattern) {
|
|
40
|
+
// Convert glob pattern to regex
|
|
41
|
+
const regexPattern = pattern
|
|
42
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
43
|
+
.replace(/\*/g, '.*') // Convert * to .*
|
|
44
|
+
.replace(/\?/g, '.'); // Convert ? to .
|
|
45
|
+
|
|
46
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
47
|
+
return regex.test(name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class TagStore {
|
|
51
|
+
/**
|
|
52
|
+
* @param {Object} fs - Filesystem interface
|
|
53
|
+
* @param {string} jjDir - Path to .jj directory
|
|
54
|
+
*/
|
|
55
|
+
constructor(fs, jjDir) {
|
|
56
|
+
this.fs = fs;
|
|
57
|
+
this.jjDir = jjDir;
|
|
58
|
+
this.tagsFile = `${jjDir}/store/tags.json`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Loads tags from storage
|
|
63
|
+
* @returns {Promise<Object>} Map of tag name to changeId
|
|
64
|
+
*/
|
|
65
|
+
async load() {
|
|
66
|
+
try {
|
|
67
|
+
const data = await this.fs.readFile(this.tagsFile, 'utf8');
|
|
68
|
+
return JSON.parse(data);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.code === 'ENOENT') {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Saves tags to storage
|
|
79
|
+
* @param {Object} tags - Map of tag name to changeId
|
|
80
|
+
* @returns {Promise<void>}
|
|
81
|
+
*/
|
|
82
|
+
async save(tags) {
|
|
83
|
+
await this.fs.writeFile(this.tagsFile, JSON.stringify(tags, null, 2));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a new tag
|
|
88
|
+
* @param {string} name - Tag name
|
|
89
|
+
* @param {string} changeId - Change ID to tag
|
|
90
|
+
* @returns {Promise<{name: string, changeId: string}>}
|
|
91
|
+
* @throws {JJError} If tag already exists or name is invalid
|
|
92
|
+
*/
|
|
93
|
+
async create(name, changeId) {
|
|
94
|
+
validateTagName(name);
|
|
95
|
+
|
|
96
|
+
const tags = await this.load();
|
|
97
|
+
|
|
98
|
+
if (tags[name]) {
|
|
99
|
+
throw new JJError('TAG_EXISTS', `Tag "${name}" already exists`, {
|
|
100
|
+
name,
|
|
101
|
+
existingChangeId: tags[name],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
tags[name] = changeId;
|
|
106
|
+
await this.save(tags);
|
|
107
|
+
|
|
108
|
+
return { name, changeId };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Lists all tags, optionally filtered by pattern
|
|
113
|
+
* @param {string} [pattern] - Optional glob pattern
|
|
114
|
+
* @returns {Promise<Array<{name: string, changeId: string}>>}
|
|
115
|
+
*/
|
|
116
|
+
async list(pattern) {
|
|
117
|
+
const tags = await this.load();
|
|
118
|
+
|
|
119
|
+
let tagNames = Object.keys(tags);
|
|
120
|
+
|
|
121
|
+
if (pattern) {
|
|
122
|
+
tagNames = tagNames.filter((name) => matchesPattern(name, pattern));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return tagNames.map((name) => ({
|
|
126
|
+
name,
|
|
127
|
+
changeId: tags[name],
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Deletes a tag
|
|
133
|
+
* @param {string} name - Tag name to delete
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
* @throws {JJError} If tag doesn't exist
|
|
136
|
+
*/
|
|
137
|
+
async delete(name) {
|
|
138
|
+
const tags = await this.load();
|
|
139
|
+
|
|
140
|
+
if (!tags[name]) {
|
|
141
|
+
throw new JJError('TAG_NOT_FOUND', `Tag "${name}" not found`, { name });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
delete tags[name];
|
|
145
|
+
await this.save(tags);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Checks if a tag exists
|
|
150
|
+
* @param {string} name - Tag name
|
|
151
|
+
* @returns {Promise<boolean>}
|
|
152
|
+
*/
|
|
153
|
+
async exists(name) {
|
|
154
|
+
const tags = await this.load();
|
|
155
|
+
return !!tags[name];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets the changeId for a tag
|
|
160
|
+
* @param {string} name - Tag name
|
|
161
|
+
* @returns {Promise<string|null>} Change ID or null if not found
|
|
162
|
+
*/
|
|
163
|
+
async get(name) {
|
|
164
|
+
const tags = await this.load();
|
|
165
|
+
return tags[name] || null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UserConfig - Manages user configuration (name, email, etc.)
|
|
3
|
+
*
|
|
4
|
+
* In real JJ, this would come from:
|
|
5
|
+
* - ~/.jjconfig.toml (global config)
|
|
6
|
+
* - .jj/repo/config.toml (repo config)
|
|
7
|
+
* - Environment variables
|
|
8
|
+
*
|
|
9
|
+
* For now, we store it in the repository.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class UserConfig {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Storage} storage - Storage manager instance
|
|
15
|
+
*/
|
|
16
|
+
constructor(storage) {
|
|
17
|
+
this.storage = storage;
|
|
18
|
+
this.config = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize with default config
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} opts - Initial config
|
|
25
|
+
* @param {string} [opts.userName='User'] - User name
|
|
26
|
+
* @param {string} [opts.userEmail='user@example.com'] - User email
|
|
27
|
+
*/
|
|
28
|
+
async init(opts = {}) {
|
|
29
|
+
this.config = {
|
|
30
|
+
user: {
|
|
31
|
+
name: opts.userName || 'User',
|
|
32
|
+
email: opts.userEmail || 'user@example.com',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
await this.save();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load config from storage
|
|
41
|
+
*/
|
|
42
|
+
async load() {
|
|
43
|
+
try {
|
|
44
|
+
const data = await this.storage.read('config.json');
|
|
45
|
+
if (data) {
|
|
46
|
+
this.config = data;
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// No config file yet, use defaults
|
|
50
|
+
await this.init();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Save config to storage
|
|
56
|
+
*/
|
|
57
|
+
async save() {
|
|
58
|
+
await this.storage.write('config.json', this.config);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get user info
|
|
63
|
+
*
|
|
64
|
+
* @returns {Object} User info { name, email }
|
|
65
|
+
*/
|
|
66
|
+
getUser() {
|
|
67
|
+
if (!this.config) {
|
|
68
|
+
return { name: 'User', email: 'user@example.com' };
|
|
69
|
+
}
|
|
70
|
+
return this.config.user;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set user info
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} user - User info
|
|
77
|
+
* @param {string} user.name - User name
|
|
78
|
+
* @param {string} user.email - User email
|
|
79
|
+
*/
|
|
80
|
+
async setUser(user) {
|
|
81
|
+
if (!this.config) {
|
|
82
|
+
await this.load();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.config.user = {
|
|
86
|
+
name: user.name,
|
|
87
|
+
email: user.email,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
await this.save();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get config value
|
|
95
|
+
*
|
|
96
|
+
* @param {string} key - Config key (dot notation supported)
|
|
97
|
+
* @returns {*} Config value
|
|
98
|
+
*/
|
|
99
|
+
get(key) {
|
|
100
|
+
if (!this.config) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parts = key.split('.');
|
|
105
|
+
let value = this.config;
|
|
106
|
+
|
|
107
|
+
for (const part of parts) {
|
|
108
|
+
value = value?.[part];
|
|
109
|
+
if (value === undefined) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Set config value
|
|
119
|
+
*
|
|
120
|
+
* @param {string} key - Config key (dot notation supported)
|
|
121
|
+
* @param {*} value - Config value
|
|
122
|
+
*/
|
|
123
|
+
async set(key, value) {
|
|
124
|
+
if (!this.config) {
|
|
125
|
+
await this.load();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parts = key.split('.');
|
|
129
|
+
const lastPart = parts.pop();
|
|
130
|
+
let target = this.config;
|
|
131
|
+
|
|
132
|
+
for (const part of parts) {
|
|
133
|
+
if (!target[part]) {
|
|
134
|
+
target[part] = {};
|
|
135
|
+
}
|
|
136
|
+
target = target[part];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
target[lastPart] = value;
|
|
140
|
+
await this.save();
|
|
141
|
+
}
|
|
142
|
+
}
|