@makerbi/remodex 1.3.8 → 1.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/package.json +2 -2
- package/src/bridge.js +112 -30
- package/src/ios-app-compatibility.js +2 -2
- package/src/macos-launch-agent.js +5 -0
- package/src/project-handler.js +134 -0
- package/src/workspace-checkpoints.js +406 -0
- package/src/workspace-handler.js +129 -18
- package/README.md +0 -483
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makerbi/remodex",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.9",
|
|
4
4
|
"description": "Local bridge between Codex and the Remodex mobile app. Run `remodex up` to start.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"remodex": "bin/remodex.js"
|
|
7
|
+
"remodex": "./bin/remodex.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
package/src/bridge.js
CHANGED
|
@@ -51,12 +51,14 @@ const { createShortPairingCode, SHORT_PAIRING_CODE_LENGTH } = require("./qr");
|
|
|
51
51
|
|
|
52
52
|
const execFileAsync = promisify(execFile);
|
|
53
53
|
const RELAY_WATCHDOG_PING_INTERVAL_MS = 10_000;
|
|
54
|
-
|
|
54
|
+
// Keep the watchdog above the relay heartbeat cadence so quiet healthy sockets survive idle gaps.
|
|
55
|
+
const RELAY_WATCHDOG_STALE_AFTER_MS = 70_000;
|
|
55
56
|
const BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
56
57
|
const STALE_RELAY_STATUS_MESSAGE = "Relay heartbeat stalled; reconnect pending.";
|
|
57
58
|
const RELAY_HISTORY_IMAGE_REFERENCE_URL = "remodex://history-image-elided";
|
|
58
|
-
const RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES =
|
|
59
|
+
const RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
59
60
|
const RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS = 24_000;
|
|
61
|
+
const RELAY_HISTORY_RECENT_TURN_TARGET = 40;
|
|
60
62
|
|
|
61
63
|
function buildRelayUserAgentHeader({ version = bridgePackageVersion } = {}) {
|
|
62
64
|
const normalizedVersion = typeof version === "string" && version.trim()
|
|
@@ -1376,8 +1378,8 @@ function normalizeNonEmptyString(value) {
|
|
|
1376
1378
|
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
1377
1379
|
}
|
|
1378
1380
|
|
|
1379
|
-
// Shrinks `thread/read` and `thread/resume` snapshots
|
|
1380
|
-
//
|
|
1381
|
+
// Shrinks `thread/read` and `thread/resume` snapshots for mobile relay delivery.
|
|
1382
|
+
// This elides bulky blobs and replaces oversized older history with a compact marker.
|
|
1381
1383
|
function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
1382
1384
|
if (requestMethod !== "thread/read" && requestMethod !== "thread/resume") {
|
|
1383
1385
|
return rawMessage;
|
|
@@ -1779,19 +1781,25 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
|
1779
1781
|
}
|
|
1780
1782
|
|
|
1781
1783
|
const turns = thread.turns;
|
|
1782
|
-
let trimmedTurns = turns.
|
|
1784
|
+
let trimmedTurns = turns.length > RELAY_HISTORY_RECENT_TURN_TARGET
|
|
1785
|
+
? turns.slice(-RELAY_HISTORY_RECENT_TURN_TARGET)
|
|
1786
|
+
: turns.slice();
|
|
1783
1787
|
while (trimmedTurns.length > 1) {
|
|
1784
|
-
trimmedTurns
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1788
|
+
if (trimmedTurns.length === turns.length) {
|
|
1789
|
+
trimmedTurns = trimmedTurns.slice(1);
|
|
1790
|
+
}
|
|
1791
|
+
const candidateThread = buildRelayHistoryCompactedThread(
|
|
1792
|
+
thread,
|
|
1793
|
+
buildRelayCompactedHistoryTurns(turns, trimmedTurns),
|
|
1794
|
+
Math.max(0, turns.length - trimmedTurns.length),
|
|
1795
|
+
trimmedTurns.length
|
|
1796
|
+
);
|
|
1790
1797
|
encoded = encodeRelayThreadPayload(parsed, candidateThread);
|
|
1791
1798
|
if (encoded != null && Buffer.byteLength(encoded, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
|
|
1792
1799
|
return encoded;
|
|
1793
1800
|
}
|
|
1794
1801
|
workingThread = candidateThread;
|
|
1802
|
+
trimmedTurns = trimmedTurns.slice(1);
|
|
1795
1803
|
}
|
|
1796
1804
|
|
|
1797
1805
|
const newestTurn = trimmedTurns[0];
|
|
@@ -1802,14 +1810,23 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
|
1802
1810
|
let trimmedItems = newestTurn.items.slice();
|
|
1803
1811
|
while (trimmedItems.length > 1) {
|
|
1804
1812
|
trimmedItems = trimmedItems.slice(1);
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
|
|
1813
|
+
const compactedTurnPrefix = buildRelayHistoryCompactionTurn(
|
|
1814
|
+
Math.max(0, turns.length - 1),
|
|
1815
|
+
1,
|
|
1816
|
+
thread
|
|
1817
|
+
);
|
|
1818
|
+
const candidateThread = buildRelayHistoryCompactedThread(
|
|
1819
|
+
thread,
|
|
1820
|
+
compactedTurnPrefix ? [compactedTurnPrefix, {
|
|
1821
|
+
...newestTurn,
|
|
1822
|
+
items: trimmedItems,
|
|
1823
|
+
}] : [{
|
|
1808
1824
|
...newestTurn,
|
|
1809
1825
|
items: trimmedItems,
|
|
1810
1826
|
}],
|
|
1811
|
-
|
|
1812
|
-
|
|
1827
|
+
Math.max(0, turns.length - 1),
|
|
1828
|
+
1
|
|
1829
|
+
);
|
|
1813
1830
|
encoded = encodeRelayThreadPayload(parsed, candidateThread);
|
|
1814
1831
|
if (encoded != null && Buffer.byteLength(encoded, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
|
|
1815
1832
|
return encoded;
|
|
@@ -1826,28 +1843,93 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
|
1826
1843
|
mostRecentItem,
|
|
1827
1844
|
RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS
|
|
1828
1845
|
);
|
|
1829
|
-
let candidateThread =
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
...newestTurn,
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1846
|
+
let candidateThread = buildRelayHistoryCompactedThread(
|
|
1847
|
+
thread,
|
|
1848
|
+
[
|
|
1849
|
+
...buildRelayCompactedHistoryTurns(turns, [newestTurn]).slice(0, -1),
|
|
1850
|
+
{
|
|
1851
|
+
...newestTurn,
|
|
1852
|
+
items: [truncatedItem],
|
|
1853
|
+
},
|
|
1854
|
+
],
|
|
1855
|
+
Math.max(0, turns.length - 1),
|
|
1856
|
+
1
|
|
1857
|
+
);
|
|
1837
1858
|
encoded = encodeRelayThreadPayload(parsed, candidateThread);
|
|
1838
1859
|
if (encoded != null && Buffer.byteLength(encoded, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
|
|
1839
1860
|
return encoded;
|
|
1840
1861
|
}
|
|
1841
1862
|
|
|
1842
|
-
candidateThread =
|
|
1863
|
+
candidateThread = buildRelayHistoryCompactedThread(
|
|
1864
|
+
thread,
|
|
1865
|
+
[
|
|
1866
|
+
...buildRelayCompactedHistoryTurns(turns, [newestTurn]).slice(0, -1),
|
|
1867
|
+
{
|
|
1868
|
+
...newestTurn,
|
|
1869
|
+
items: [compactHistoryItemForRelay(mostRecentItem, RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS)],
|
|
1870
|
+
},
|
|
1871
|
+
],
|
|
1872
|
+
Math.max(0, turns.length - 1),
|
|
1873
|
+
1
|
|
1874
|
+
);
|
|
1875
|
+
return encodeRelayThreadPayload(parsed, candidateThread);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function buildRelayHistoryCompactedThread(thread, turns, omittedTurnCount, keptTurnCount) {
|
|
1879
|
+
return {
|
|
1843
1880
|
...thread,
|
|
1844
|
-
turns
|
|
1845
|
-
...newestTurn,
|
|
1846
|
-
items: [compactHistoryItemForRelay(mostRecentItem, RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS)],
|
|
1847
|
-
}],
|
|
1881
|
+
turns,
|
|
1848
1882
|
historyTailTruncatedForRelay: true,
|
|
1883
|
+
remodexHistoryCompacted: omittedTurnCount > 0,
|
|
1884
|
+
remodexOmittedTurnCount: omittedTurnCount,
|
|
1885
|
+
remodexKeptTurnCount: keptTurnCount,
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function buildRelayCompactedHistoryTurns(allTurns, keptTurns) {
|
|
1890
|
+
const omittedTurnCount = Math.max(0, allTurns.length - keptTurns.length);
|
|
1891
|
+
const compactionTurn = buildRelayHistoryCompactionTurn(
|
|
1892
|
+
omittedTurnCount,
|
|
1893
|
+
keptTurns.length,
|
|
1894
|
+
allTurns[0]
|
|
1895
|
+
);
|
|
1896
|
+
return compactionTurn ? [compactionTurn, ...keptTurns] : keptTurns;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function buildRelayHistoryCompactionTurn(omittedTurnCount, keptTurnCount, idSource = {}) {
|
|
1900
|
+
if (omittedTurnCount <= 0) {
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const baseId = normalizeNonEmptyString(idSource?.id)
|
|
1905
|
+
|| normalizeNonEmptyString(idSource?.turnId)
|
|
1906
|
+
|| normalizeNonEmptyString(idSource?.turn_id)
|
|
1907
|
+
|| "history";
|
|
1908
|
+
const text = [
|
|
1909
|
+
"Earlier conversation compacted for mobile loading.",
|
|
1910
|
+
"",
|
|
1911
|
+
`Older turns omitted: ${omittedTurnCount}`,
|
|
1912
|
+
`Recent turns kept: ${keptTurnCount}`,
|
|
1913
|
+
"Full history remains available on the Mac runtime.",
|
|
1914
|
+
].join("\n");
|
|
1915
|
+
|
|
1916
|
+
return {
|
|
1917
|
+
id: `remodex-history-compacted-${baseId}`,
|
|
1918
|
+
remodexSynthetic: true,
|
|
1919
|
+
remodexHistoryCompacted: true,
|
|
1920
|
+
remodexOmittedTurnCount: omittedTurnCount,
|
|
1921
|
+
remodexKeptTurnCount: keptTurnCount,
|
|
1922
|
+
items: [
|
|
1923
|
+
{
|
|
1924
|
+
id: `remodex-history-compacted-item-${baseId}`,
|
|
1925
|
+
type: "assistant_message",
|
|
1926
|
+
role: "assistant",
|
|
1927
|
+
text,
|
|
1928
|
+
remodexSynthetic: true,
|
|
1929
|
+
remodexHistoryCompacted: true,
|
|
1930
|
+
},
|
|
1931
|
+
],
|
|
1849
1932
|
};
|
|
1850
|
-
return encodeRelayThreadPayload(parsed, candidateThread);
|
|
1851
1933
|
}
|
|
1852
1934
|
|
|
1853
1935
|
function encodeRelayThreadPayload(parsed, thread) {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// Exports: version comparison + bridge/iPhone compatibility helpers
|
|
5
5
|
// Depends on: none
|
|
6
6
|
|
|
7
|
-
const MINIMUM_SUPPORTED_IOS_APP_VERSION = "1.
|
|
8
|
-
const IOS_APP_COMPATIBILITY_GATE_BRIDGE_VERSION = "1.3.
|
|
7
|
+
const MINIMUM_SUPPORTED_IOS_APP_VERSION = "1.5";
|
|
8
|
+
const IOS_APP_COMPATIBILITY_GATE_BRIDGE_VERSION = "1.3.9";
|
|
9
9
|
const LEGACY_BRIDGE_VERSION_FOR_IOS_1_0 = "1.3.7";
|
|
10
10
|
const LEGACY_BRIDGE_DOWNGRADE_COMMAND = `npm install -g remodex@${LEGACY_BRIDGE_VERSION_FOR_IOS_1_0}`;
|
|
11
11
|
const NOTICE_BOX_WIDTH = 74;
|
|
@@ -334,6 +334,11 @@ function restartLaunchAgent({
|
|
|
334
334
|
launchAgentDomain(env),
|
|
335
335
|
plistPath,
|
|
336
336
|
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
337
|
+
execFileSyncImpl("launchctl", [
|
|
338
|
+
"kickstart",
|
|
339
|
+
"-k",
|
|
340
|
+
launchAgentLabelDomain(env),
|
|
341
|
+
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
337
342
|
}
|
|
338
343
|
|
|
339
344
|
function bootoutLaunchAgent({
|
package/src/project-handler.js
CHANGED
|
@@ -9,6 +9,9 @@ const os = require("os");
|
|
|
9
9
|
const path = require("path");
|
|
10
10
|
|
|
11
11
|
const DEFAULT_DIRECTORY_LIMIT = 200;
|
|
12
|
+
const DEFAULT_DIRECTORY_SEARCH_LIMIT = 80;
|
|
13
|
+
const DEFAULT_DIRECTORY_SEARCH_MAX_DEPTH = 8;
|
|
14
|
+
const DEFAULT_DIRECTORY_SEARCH_MAX_VISITED = 5000;
|
|
12
15
|
const DEFAULT_HIDDEN_DIRECTORY_NAMES = new Set(["Library"]);
|
|
13
16
|
|
|
14
17
|
// ─── ENTRY POINT ─────────────────────────────────────────────
|
|
@@ -57,6 +60,8 @@ async function handleProjectMethod(method, params, options = {}) {
|
|
|
57
60
|
return projectQuickLocations(options);
|
|
58
61
|
case "project/listDirectory":
|
|
59
62
|
return projectListDirectory(params, options);
|
|
63
|
+
case "project/searchDirectories":
|
|
64
|
+
return projectSearchDirectories(params, options);
|
|
60
65
|
case "project/validatePath":
|
|
61
66
|
return projectValidatePath(params, options);
|
|
62
67
|
case "project/createDirectory":
|
|
@@ -112,6 +117,32 @@ async function projectListDirectory(params, options = {}) {
|
|
|
112
117
|
};
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
async function projectSearchDirectories(params, options = {}) {
|
|
121
|
+
const requestedPath = readString(params.path) || resolveHomeDir(options);
|
|
122
|
+
const query = readString(params.query);
|
|
123
|
+
const directory = await requireUsableDirectory(requestedPath, options);
|
|
124
|
+
if (!query) {
|
|
125
|
+
return {
|
|
126
|
+
path: directory.path,
|
|
127
|
+
entries: [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const includeHidden = params.includeHidden === true;
|
|
132
|
+
const entries = await searchDirectoryEntries(directory.path, query, {
|
|
133
|
+
...options,
|
|
134
|
+
includeHidden,
|
|
135
|
+
limit: normalizeSearchLimit(params.limit),
|
|
136
|
+
maxDepth: normalizeSearchDepth(params.maxDepth),
|
|
137
|
+
maxVisited: normalizeSearchVisitedLimit(params.maxVisited),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
path: directory.path,
|
|
142
|
+
entries,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
115
146
|
async function projectValidatePath(params, options = {}) {
|
|
116
147
|
const requestedPath = readString(params.path);
|
|
117
148
|
if (!requestedPath) {
|
|
@@ -181,6 +212,62 @@ async function readDirectoryEntries(directoryPath, options = {}) {
|
|
|
181
212
|
.slice(0, options.limit || DEFAULT_DIRECTORY_LIMIT);
|
|
182
213
|
}
|
|
183
214
|
|
|
215
|
+
async function searchDirectoryEntries(rootPath, query, options = {}) {
|
|
216
|
+
const tokens = searchTokens(query);
|
|
217
|
+
if (!tokens.length) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const limit = options.limit || DEFAULT_DIRECTORY_SEARCH_LIMIT;
|
|
222
|
+
const maxDepth = options.maxDepth ?? DEFAULT_DIRECTORY_SEARCH_MAX_DEPTH;
|
|
223
|
+
const maxVisited = options.maxVisited || DEFAULT_DIRECTORY_SEARCH_MAX_VISITED;
|
|
224
|
+
const queue = [{ directoryPath: rootPath, depth: 0 }];
|
|
225
|
+
const visitedDirectories = new Set([realpathSyncIfAvailable(rootPath) || rootPath]);
|
|
226
|
+
const matches = [];
|
|
227
|
+
let visitedCount = 0;
|
|
228
|
+
|
|
229
|
+
while (queue.length && matches.length < limit && visitedCount < maxVisited) {
|
|
230
|
+
const { directoryPath, depth } = queue.shift();
|
|
231
|
+
visitedCount += 1;
|
|
232
|
+
|
|
233
|
+
let dirents;
|
|
234
|
+
try {
|
|
235
|
+
dirents = await fs.promises.readdir(directoryPath, { withFileTypes: true });
|
|
236
|
+
} catch {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (const dirent of sortedDirents(dirents)) {
|
|
241
|
+
if (!options.includeHidden && isHiddenDirectoryName(dirent.name)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const childPath = path.join(directoryPath, dirent.name);
|
|
246
|
+
const directory = await directoryEntryForPath(childPath, dirent, options);
|
|
247
|
+
if (!directory) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (directoryMatchesSearch(directory, tokens)) {
|
|
252
|
+
matches.push(directory);
|
|
253
|
+
if (matches.length >= limit) {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!dirent.isSymbolicLink() && depth < maxDepth) {
|
|
259
|
+
const realPath = directory.path;
|
|
260
|
+
if (!visitedDirectories.has(realPath)) {
|
|
261
|
+
visitedDirectories.add(realPath);
|
|
262
|
+
queue.push({ directoryPath: realPath, depth: depth + 1 });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return matches;
|
|
269
|
+
}
|
|
270
|
+
|
|
184
271
|
async function directoryEntryForPath(candidatePath, dirent, options = {}) {
|
|
185
272
|
if (!dirent.isDirectory() && !dirent.isSymbolicLink()) {
|
|
186
273
|
return null;
|
|
@@ -325,6 +412,52 @@ function normalizeLimit(rawLimit) {
|
|
|
325
412
|
return Math.min(Math.floor(numericLimit), DEFAULT_DIRECTORY_LIMIT);
|
|
326
413
|
}
|
|
327
414
|
|
|
415
|
+
function normalizeSearchLimit(rawLimit) {
|
|
416
|
+
const numericLimit = Number(rawLimit);
|
|
417
|
+
if (!Number.isFinite(numericLimit) || numericLimit <= 0) {
|
|
418
|
+
return DEFAULT_DIRECTORY_SEARCH_LIMIT;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return Math.min(Math.floor(numericLimit), DEFAULT_DIRECTORY_SEARCH_LIMIT);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function normalizeSearchDepth(rawDepth) {
|
|
425
|
+
const numericDepth = Number(rawDepth);
|
|
426
|
+
if (!Number.isFinite(numericDepth) || numericDepth < 0) {
|
|
427
|
+
return DEFAULT_DIRECTORY_SEARCH_MAX_DEPTH;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return Math.min(Math.floor(numericDepth), DEFAULT_DIRECTORY_SEARCH_MAX_DEPTH);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function normalizeSearchVisitedLimit(rawLimit) {
|
|
434
|
+
const numericLimit = Number(rawLimit);
|
|
435
|
+
if (!Number.isFinite(numericLimit) || numericLimit <= 0) {
|
|
436
|
+
return DEFAULT_DIRECTORY_SEARCH_MAX_VISITED;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return Math.min(Math.floor(numericLimit), DEFAULT_DIRECTORY_SEARCH_MAX_VISITED);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function sortedDirents(dirents) {
|
|
443
|
+
return [...dirents].sort((left, right) => (
|
|
444
|
+
left.name.localeCompare(right.name, undefined, { sensitivity: "base" })
|
|
445
|
+
));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function searchTokens(query) {
|
|
449
|
+
return query
|
|
450
|
+
.toLowerCase()
|
|
451
|
+
.split(/\s+/)
|
|
452
|
+
.map((token) => token.trim())
|
|
453
|
+
.filter(Boolean);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function directoryMatchesSearch(directory, tokens) {
|
|
457
|
+
const haystack = directory.name.toLowerCase();
|
|
458
|
+
return tokens.every((token) => haystack.includes(token));
|
|
459
|
+
}
|
|
460
|
+
|
|
328
461
|
function resolveHomeDir(options = {}) {
|
|
329
462
|
return options.homeDir || os.homedir();
|
|
330
463
|
}
|
|
@@ -353,6 +486,7 @@ module.exports = {
|
|
|
353
486
|
handleProjectMethod,
|
|
354
487
|
projectQuickLocations,
|
|
355
488
|
projectListDirectory,
|
|
489
|
+
projectSearchDirectories,
|
|
356
490
|
projectValidatePath,
|
|
357
491
|
projectCreateDirectory,
|
|
358
492
|
validateDirectory,
|