@oar-sh/oar 0.9.22 → 0.9.24
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 +258 -0
- package/README.md +9 -9
- package/bin/oar.js +82 -4
- package/package.json +16 -15
- package/server/agent-sdk-loader.mjs +116 -0
- package/server/agent-sdks/claude/entry.mjs +4 -0
- package/server/agent-sdks/claude/package-lock.json +1438 -0
- package/server/agent-sdks/claude/package.json +13 -0
- package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
- package/server/agent-sdks/cursor/entry.mjs +3 -0
- package/server/agent-sdks/cursor/package-lock.json +171 -0
- package/server/agent-sdks/cursor/package.json +11 -0
- package/server/agent-sdks/koffi/entry.cjs +2 -0
- package/server/agent-sdks/koffi/package-lock.json +368 -0
- package/server/agent-sdks/koffi/package.json +10 -0
- package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
- package/server/claude-worker/claude-preview-tool.mjs +5 -2
- package/server/claude-worker/claude-remote-relay-tool.mjs +5 -2
- package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +7 -4
- package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
- package/server/public/app/agent-sdk-ui.js +349 -0
- package/server/public/app/api-client.js +91 -8
- package/server/public/app/bootstrap.js +204 -69
- package/server/public/app/model-refresh-ui.mjs +413 -0
- package/server/public/app/no-conversation-state.mjs +5 -2
- package/server/public/app/provider-readiness.mjs +67 -4
- package/server/public/app/settings-modal.js +167 -0
- package/server/public/app/socket-handlers.js +21 -0
- package/server/public/index.html +95 -2
- package/server/routes/agent-sdk-routes.mjs +73 -0
- package/server/routes/messages-routes.mjs +102 -37
- package/server/routes/sessions-routes.mjs +236 -110
- package/server/server-runtime.mjs +416 -121
- package/server/services/about-service.mjs +26 -6
- package/server/services/agent-sdk-launch-gate.mjs +124 -0
- package/server/services/agent-sdk-paths.mjs +307 -0
- package/server/services/agent-sdk-store.mjs +1211 -0
- package/server/services/claude-api-settings-service.mjs +54 -17
- package/server/services/cli-install-service.mjs +3 -2
- package/server/services/copilot-model-discovery-service.mjs +7 -1
- package/server/services/database-file.mjs +17 -1
- package/server/services/deepseek-settings-service.mjs +58 -20
- package/server/services/earlier-database-service.mjs +5 -3
- package/server/services/file-serving.mjs +13 -0
- package/server/services/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/native-addon-build-service.mjs +224 -0
- package/server/services/oar-cli-helpers.mjs +76 -10
- package/server/services/oar-state-migration-service.mjs +2 -12
- package/server/services/oar-state-root.mjs +17 -0
- package/server/services/provider-model-refresh.mjs +326 -0
- package/server/services/provider-model-state.mjs +395 -0
- package/server/services/relay-console-log.mjs +2 -3
- package/server/services/relay-singleton-guard.mjs +72 -10
- package/server/services/remote-relay-loopback.mjs +3 -3
- package/server/services/sdk-session-sync-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +13 -0
- package/server/services/session-worker-supervisor-service.mjs +19 -1
- package/server/services/session-worker-websocket-service.mjs +15 -0
- package/server/services/update-install-service.mjs +34 -3
- package/server/services/worker-launch-note-service.mjs +14 -2
- package/server/services/worker-recycle-service.mjs +145 -11
- package/shared/model-refresh-schedule.mjs +120 -0
- package/shared/worker-runtime/direct-openai-image.mjs +8 -4
- package/server/services/package-data-rescue-preinstall.mjs +0 -22
- package/server/services/package-data-rescue.mjs +0 -139
- package/server/tty-console-bootstrap.mjs +0 -75
|
@@ -28,7 +28,13 @@
|
|
|
28
28
|
// * curated ids only seed an EMPTY catalog; once an authoritative snapshot
|
|
29
29
|
// (a running engine or the server-side discovery) has listed the models,
|
|
30
30
|
// anything absent from it is marked `unavailable` (enabled rows stay
|
|
31
|
-
// listed so a picker can show them greyed; disabled ones are pruned)
|
|
31
|
+
// listed so a picker can show them greyed; disabled ones are pruned);
|
|
32
|
+
// * with a model map (`copilot_model_state`, provider-model-state.mjs) a row
|
|
33
|
+
// seen for the first time starts as the user would expect: off when the
|
|
34
|
+
// user turned that row off before (a pruned row is remembered), like the
|
|
35
|
+
// model's other rows for a new effort level, and as the "Turn new models
|
|
36
|
+
// on automatically" switch says for a whole new model, which is also
|
|
37
|
+
// flagged new. Without one (tests, older wiring) new rows start off.
|
|
32
38
|
import { humanizeModelLabel } from '../public/app/model-selector-options.mjs';
|
|
33
39
|
import { latestModelCatalogRefresh } from '../../shared/model-catalog-freshness.mjs';
|
|
34
40
|
import {
|
|
@@ -39,6 +45,7 @@ import {
|
|
|
39
45
|
import { KNOWN_REASONING_EFFORTS, MODEL_PICKER_CATEGORIES } from '../../shared/model-descriptors.mjs';
|
|
40
46
|
import { copilotCatalogIndexByModel, inferCopilotVendor } from '../../shared/copilot-model-order.mjs';
|
|
41
47
|
import { selectModelIdsForVariantRefresh } from '../../shared/model-refresh.mjs';
|
|
48
|
+
import { decideNewCopilotVariant, migrateCopilotModelState } from './provider-model-state.mjs';
|
|
42
49
|
|
|
43
50
|
export const SUPPORTED_REASONING_EFFORTS = [...KNOWN_REASONING_EFFORTS];
|
|
44
51
|
/**
|
|
@@ -471,6 +478,14 @@ export function createModelVariantCatalogService({
|
|
|
471
478
|
autoModelSentinel = 'auto',
|
|
472
479
|
// (args: string[]) => Promise<string>; the `copilot help` text fallback.
|
|
473
480
|
runCopilotCliCommand = async () => '',
|
|
481
|
+
// { readCopilot(), writeCopilot(state) } (provider-model-state.mjs); null
|
|
482
|
+
// keeps the old rule: every new row starts off.
|
|
483
|
+
modelStateStore = null,
|
|
484
|
+
// (baseModelIds, on) — whole new models were added (the toast).
|
|
485
|
+
onNewModels = () => {},
|
|
486
|
+
// ({ source }) — a snapshot that read the runtime's real list arrived
|
|
487
|
+
// (counts as a Copilot model refresh).
|
|
488
|
+
onAuthoritativeSnapshot = () => {},
|
|
474
489
|
} = {}) {
|
|
475
490
|
const DEFAULT_MODEL = String(defaultModel || 'gpt-5.4-mini').trim() || 'gpt-5.4-mini';
|
|
476
491
|
const AUTO_MODEL_SENTINEL = String(autoModelSentinel || 'auto');
|
|
@@ -511,6 +526,56 @@ export function createModelVariantCatalogService({
|
|
|
511
526
|
return modelSelectorSql.listEnabledVariants.all().map((row) => normalizeModelVariantRow(row)).sort(compareVariantRows);
|
|
512
527
|
}
|
|
513
528
|
|
|
529
|
+
/**
|
|
530
|
+
* The Copilot model map, built from the table the first time (every model
|
|
531
|
+
* there known, every disabled row remembered as off). Null without a store.
|
|
532
|
+
*/
|
|
533
|
+
function loadCopilotModelState(rows = null) {
|
|
534
|
+
if (!modelStateStore) return null;
|
|
535
|
+
try {
|
|
536
|
+
const stored = modelStateStore.readCopilot();
|
|
537
|
+
if (stored) return stored;
|
|
538
|
+
const migrated = migrateCopilotModelState(rows || listModelVariantRows());
|
|
539
|
+
modelStateStore.writeCopilot(migrated);
|
|
540
|
+
return migrated;
|
|
541
|
+
} catch {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function saveCopilotModelState(state) {
|
|
547
|
+
try {
|
|
548
|
+
modelStateStore?.writeCopilot?.(state);
|
|
549
|
+
} catch {
|
|
550
|
+
/* the rows are written; the map catches up on the next change */
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** `{ autoEnableNew, unseen }` for Select Models and the menu dot. */
|
|
555
|
+
function getCopilotModelState() {
|
|
556
|
+
const state = loadCopilotModelState();
|
|
557
|
+
const bases = new Set(listModelVariantRows().map((row) => row.baseModelId));
|
|
558
|
+
return {
|
|
559
|
+
autoEnableNew: state ? state.autoEnableNew : false,
|
|
560
|
+
unseen: state ? state.unseen.filter((baseModelId) => bases.has(baseModelId)) : [],
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function setCopilotAutoEnableNew(value) {
|
|
565
|
+
const state = loadCopilotModelState();
|
|
566
|
+
if (!state || typeof value !== 'boolean') return getCopilotModelState();
|
|
567
|
+
saveCopilotModelState({ ...state, autoEnableNew: value });
|
|
568
|
+
return getCopilotModelState();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** Clears the "New" flags; true when one was set. */
|
|
572
|
+
function markCopilotModelsSeen() {
|
|
573
|
+
const state = loadCopilotModelState();
|
|
574
|
+
if (!state || !state.unseen.length) return false;
|
|
575
|
+
saveCopilotModelState({ ...state, unseen: [] });
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
|
|
514
579
|
/** Stored metadata per base model (first row wins; all rows carry the same). */
|
|
515
580
|
function storedMetadataByModel(rows = listModelVariantRows()) {
|
|
516
581
|
const byModel = {};
|
|
@@ -772,12 +837,21 @@ export function createModelVariantCatalogService({
|
|
|
772
837
|
const existingEnabled = new Map(existingRows.map((row) => [row.variantId, row.enabled ? 1 : 0]));
|
|
773
838
|
const incomingIds = new Set(normalizedEntries.map((entry) => entry.variantId));
|
|
774
839
|
const incomingBaseIds = new Set(normalizedEntries.map((entry) => entry.baseModelId));
|
|
840
|
+
const enabledBaseIds = new Set(existingRows.filter((row) => row.enabled).map((row) => row.baseModelId));
|
|
841
|
+
const modelState = loadCopilotModelState(existingRows);
|
|
842
|
+
const newBaseIds = new Set();
|
|
843
|
+
const prunedDisabledIds = new Set();
|
|
775
844
|
const nowIso = new Date().toISOString();
|
|
776
845
|
const tx = db.transaction(() => {
|
|
777
846
|
for (const entry of normalizedEntries) {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
847
|
+
let enabled = entry.enabled ? 1 : 0;
|
|
848
|
+
if (preserveEnabled && existingEnabled.has(entry.variantId)) {
|
|
849
|
+
enabled = existingEnabled.get(entry.variantId);
|
|
850
|
+
} else if (modelState && !existingEnabled.has(entry.variantId)) {
|
|
851
|
+
const decision = decideNewCopilotVariant(modelState, entry, { siblingEnabled: enabledBaseIds.has(entry.baseModelId) });
|
|
852
|
+
enabled = decision.enabled ? 1 : 0;
|
|
853
|
+
if (decision.newModel) newBaseIds.add(entry.baseModelId);
|
|
854
|
+
}
|
|
781
855
|
runVariantUpsert(entry, enabled, nowIso);
|
|
782
856
|
}
|
|
783
857
|
const orphanedEnabledBases = new Set();
|
|
@@ -788,6 +862,7 @@ export function createModelVariantCatalogService({
|
|
|
788
862
|
// The runtime told us this model's exact levels; a row for a
|
|
789
863
|
// level it does not offer is stale, not "unavailable".
|
|
790
864
|
if (row.enabled) orphanedEnabledBases.add(row.baseModelId);
|
|
865
|
+
else prunedDisabledIds.add(row.variantId);
|
|
791
866
|
modelSelectorSql.deleteVariant.run(row.variantId);
|
|
792
867
|
} else {
|
|
793
868
|
runVariantUpsert({ ...row, releaseStatus: 'unavailable' }, row.enabled, nowIso);
|
|
@@ -801,6 +876,8 @@ export function createModelVariantCatalogService({
|
|
|
801
876
|
}
|
|
802
877
|
// Variants that are both disabled and fully absent from the incoming
|
|
803
878
|
// base set are no longer relevant; prune them so they do not linger.
|
|
879
|
+
// The model map remembers them as off for when they come back.
|
|
880
|
+
prunedDisabledIds.add(row.variantId);
|
|
804
881
|
modelSelectorSql.deleteVariant.run(row.variantId);
|
|
805
882
|
}
|
|
806
883
|
// A selection must not silently vanish because its effort level did:
|
|
@@ -822,6 +899,21 @@ export function createModelVariantCatalogService({
|
|
|
822
899
|
}
|
|
823
900
|
});
|
|
824
901
|
tx();
|
|
902
|
+
if (modelState) {
|
|
903
|
+
saveCopilotModelState({
|
|
904
|
+
...modelState,
|
|
905
|
+
knownModels: [...new Set([...modelState.knownModels, ...incomingBaseIds])],
|
|
906
|
+
disabledVariants: [...new Set([...modelState.disabledVariants, ...prunedDisabledIds])],
|
|
907
|
+
unseen: [...new Set([...modelState.unseen, ...newBaseIds])],
|
|
908
|
+
});
|
|
909
|
+
if (newBaseIds.size) {
|
|
910
|
+
try {
|
|
911
|
+
onNewModels([...newBaseIds], modelState.autoEnableNew);
|
|
912
|
+
} catch {
|
|
913
|
+
/* the toast is best-effort */
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
825
917
|
recomputeSortOrder();
|
|
826
918
|
return getModelVariantSelectorState();
|
|
827
919
|
}
|
|
@@ -839,23 +931,49 @@ export function createModelVariantCatalogService({
|
|
|
839
931
|
return reconcileModelVariantRows(entries, { source, error, preserveEnabled, absentBasePolicy: 'reconcile', knownEffortBases });
|
|
840
932
|
}
|
|
841
933
|
|
|
842
|
-
|
|
934
|
+
/**
|
|
935
|
+
* The Select Models save. With `shownVariantIds` (the rows the browser
|
|
936
|
+
* drew) only those rows are set, on when listed in `variantIds`; every
|
|
937
|
+
* other row keeps its state, so a row a refresh added after the modal was
|
|
938
|
+
* drawn is not turned off. Without it (an older page) the list is the whole
|
|
939
|
+
* enabled set, as before.
|
|
940
|
+
*/
|
|
941
|
+
function setEnabledModelVariants(variantIds = [], { shownVariantIds = null } = {}) {
|
|
843
942
|
if (!modelSelectorSql?.disableAllVariants?.run || !modelSelectorSql?.enableVariant?.run) {
|
|
844
943
|
return getModelVariantSelectorState();
|
|
845
944
|
}
|
|
846
945
|
const nextEnabled = new Set(
|
|
847
946
|
Array.isArray(variantIds) ? variantIds.map((value) => String(value || '').trim()).filter(Boolean) : [],
|
|
848
947
|
);
|
|
948
|
+
// Turning every row off would leave the pickers with the fallback model
|
|
949
|
+
// only; the route refuses it, and so does this.
|
|
950
|
+
if (!nextEnabled.size) return getModelVariantSelectorState();
|
|
849
951
|
const existingRows = listModelVariantRows();
|
|
952
|
+
const shown = Array.isArray(shownVariantIds)
|
|
953
|
+
? new Set(shownVariantIds.map((value) => String(value || '').trim()).filter(Boolean))
|
|
954
|
+
: null;
|
|
955
|
+
const touchedRows = shown ? existingRows.filter((row) => shown.has(row.variantId)) : existingRows;
|
|
956
|
+
const modelState = loadCopilotModelState(existingRows);
|
|
850
957
|
const nowIso = new Date().toISOString();
|
|
851
958
|
const tx = db.transaction(() => {
|
|
852
|
-
modelSelectorSql.disableAllVariants.run(nowIso);
|
|
853
|
-
for (const row of
|
|
854
|
-
if (
|
|
855
|
-
modelSelectorSql.
|
|
959
|
+
if (!shown) modelSelectorSql.disableAllVariants.run(nowIso);
|
|
960
|
+
for (const row of touchedRows) {
|
|
961
|
+
if (nextEnabled.has(row.variantId)) modelSelectorSql.enableVariant.run(nowIso, row.variantId);
|
|
962
|
+
else if (shown) modelSelectorSql.disableVariant.run(nowIso, row.variantId);
|
|
856
963
|
}
|
|
857
964
|
});
|
|
858
965
|
tx();
|
|
966
|
+
if (modelState) {
|
|
967
|
+
// A row turned on is forgotten as off; a row turned off is remembered.
|
|
968
|
+
const touchedIds = new Set(touchedRows.map((row) => row.variantId));
|
|
969
|
+
saveCopilotModelState({
|
|
970
|
+
...modelState,
|
|
971
|
+
disabledVariants: [...new Set([
|
|
972
|
+
...modelState.disabledVariants.filter((variantId) => !(touchedIds.has(variantId) && nextEnabled.has(variantId))),
|
|
973
|
+
...touchedRows.filter((row) => !nextEnabled.has(row.variantId)).map((row) => row.variantId),
|
|
974
|
+
])],
|
|
975
|
+
});
|
|
976
|
+
}
|
|
859
977
|
return getModelVariantSelectorState();
|
|
860
978
|
}
|
|
861
979
|
|
|
@@ -897,6 +1015,13 @@ export function createModelVariantCatalogService({
|
|
|
897
1015
|
refreshedAt: receivedMetadata ? new Date().toISOString() : modelCatalog.refreshedAt,
|
|
898
1016
|
error: snapshot.error ? String(snapshot.error).trim().slice(0, 300) : null,
|
|
899
1017
|
};
|
|
1018
|
+
if (authoritative) {
|
|
1019
|
+
try {
|
|
1020
|
+
onAuthoritativeSnapshot({ source });
|
|
1021
|
+
} catch {
|
|
1022
|
+
/* the refresh clock is best-effort */
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
900
1025
|
if (modelSelectorSql?.upsertVariant?.run) {
|
|
901
1026
|
const existingRows = listModelVariantRows();
|
|
902
1027
|
const stored = storedMetadataByModel(existingRows);
|
|
@@ -1074,6 +1199,7 @@ export function createModelVariantCatalogService({
|
|
|
1074
1199
|
updateSortOrderForBase: db.prepare(`UPDATE model_variants SET sort_order = ? WHERE base_model_id = ?`),
|
|
1075
1200
|
disableAllVariants: db.prepare(`UPDATE model_variants SET enabled = 0, updated_at = ?`),
|
|
1076
1201
|
enableVariant: db.prepare(`UPDATE model_variants SET enabled = 1, updated_at = ? WHERE variant_id = ?`),
|
|
1202
|
+
disableVariant: db.prepare(`UPDATE model_variants SET enabled = 0, updated_at = ? WHERE variant_id = ?`),
|
|
1077
1203
|
deleteVariant: db.prepare(`DELETE FROM model_variants WHERE variant_id = ?`),
|
|
1078
1204
|
getSelectorState: db.prepare(`SELECT source, refreshed_at, error, updated_at FROM model_selector_state WHERE id = 1 LIMIT 1`),
|
|
1079
1205
|
upsertSelectorState: db.prepare(`
|
|
@@ -1153,6 +1279,9 @@ export function createModelVariantCatalogService({
|
|
|
1153
1279
|
}
|
|
1154
1280
|
normalizeLegacyVariantIdsTx();
|
|
1155
1281
|
recomputeSortOrder();
|
|
1282
|
+
// The model map of an install from before it is built from the table as
|
|
1283
|
+
// it stands now, before any snapshot adds a row.
|
|
1284
|
+
loadCopilotModelState();
|
|
1156
1285
|
}
|
|
1157
1286
|
|
|
1158
1287
|
return {
|
|
@@ -1175,6 +1304,9 @@ export function createModelVariantCatalogService({
|
|
|
1175
1304
|
getModelVariantSelectorState,
|
|
1176
1305
|
upsertModelVariantCatalogEntries,
|
|
1177
1306
|
setEnabledModelVariants,
|
|
1307
|
+
getCopilotModelState,
|
|
1308
|
+
setCopilotAutoEnableNew,
|
|
1309
|
+
markCopilotModelsSeen,
|
|
1178
1310
|
refreshModelVariantCatalogFromCli,
|
|
1179
1311
|
parseModelsFromHelpConfigOutput,
|
|
1180
1312
|
parseReasoningEffortsFromHelpOutput,
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* better-sqlite3 13 ships its binary for linux (glibc and musl), darwin and
|
|
3
|
+
* win32 on x64 and arm64, and runs no install script. Anywhere else (Android
|
|
4
|
+
* under Termux, 32-bit ARM, FreeBSD) the first database open finds no binary
|
|
5
|
+
* and throws: the binding has to be compiled from the sources the package
|
|
6
|
+
* ships. OAR runs that build itself, after an update (the Update button,
|
|
7
|
+
* `oar update`) and as `oar build-native`, never as an install script of its
|
|
8
|
+
* own package: one would bring back the install-script warning every package
|
|
9
|
+
* scanner shows for OAR, and npm 12 skips it unless the user allows it.
|
|
10
|
+
*
|
|
11
|
+
* The build is node-gyp's, the one npm bundles (npx's as the fallback), in the
|
|
12
|
+
* package's own folder with `--force_build=1`, as the package's
|
|
13
|
+
* `build-release` script runs it. It needs Python 3, make and a C++ compiler.
|
|
14
|
+
*/
|
|
15
|
+
import { spawn as defaultSpawn } from 'child_process';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import { createRequire } from 'module';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
|
|
20
|
+
import { killTree, runToCompletion, tailOf } from './cli-process-runner.mjs';
|
|
21
|
+
|
|
22
|
+
export const BETTER_SQLITE3_PREBUILD_PLATFORMS = Object.freeze(['linux', 'darwin', 'win32']);
|
|
23
|
+
export const BETTER_SQLITE3_PREBUILD_ARCHS = Object.freeze(['x64', 'arm64']);
|
|
24
|
+
export const NATIVE_BUILD_COMMAND = 'oar build-native';
|
|
25
|
+
// sqlite3.c is one large file: a phone takes minutes for it.
|
|
26
|
+
export const NATIVE_BUILD_TIMEOUT_MS = 30 * 60_000;
|
|
27
|
+
export const TERMUX_BUILD_TOOLS = 'pkg install python clang make';
|
|
28
|
+
|
|
29
|
+
const BINDING_FILE = path.join('build', 'Release', 'better_sqlite3.node');
|
|
30
|
+
|
|
31
|
+
/** True where better-sqlite3 has no prebuilt binary and must be compiled. */
|
|
32
|
+
export function needsSourceBuild({ platform = process.platform, arch = process.arch } = {}) {
|
|
33
|
+
return !(BETTER_SQLITE3_PREBUILD_PLATFORMS.includes(platform) && BETTER_SQLITE3_PREBUILD_ARCHS.includes(arch));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The folder of the better-sqlite3 this package loads, or null. */
|
|
37
|
+
export function resolveBetterSqlite3Dir({ packageRoot, requireFrom = createRequire } = {}) {
|
|
38
|
+
try {
|
|
39
|
+
const require = requireFrom(path.join(path.resolve(String(packageRoot || '.')), 'package.json'));
|
|
40
|
+
return path.dirname(require.resolve('better-sqlite3/package.json'));
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The error better-sqlite3 throws when it finds neither a prebuilt nor a built binary. */
|
|
47
|
+
export function isMissingSqliteBindingError(error) {
|
|
48
|
+
const code = String(error?.code || '');
|
|
49
|
+
return (code === 'MODULE_NOT_FOUND' || code === 'ERR_DLOPEN_FAILED')
|
|
50
|
+
&& /better_sqlite3\.node|better-sqlite3[\\/]prebuilds[\\/][^\\/]+\.node/.test(String(error?.message || ''));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The lines a relay prints, instead of a stack trace, when it cannot open a
|
|
55
|
+
* database because the binding is missing.
|
|
56
|
+
*/
|
|
57
|
+
export function missingSqliteBindingLines({ platform = process.platform, arch = process.arch, error = null } = {}) {
|
|
58
|
+
const target = `${platform}-${arch}`;
|
|
59
|
+
if (!needsSourceBuild({ platform, arch })) {
|
|
60
|
+
// There, but not loadable on this system. better-sqlite3 loads its
|
|
61
|
+
// prebuilt binding before a built one, so no build here would be used.
|
|
62
|
+
if (String(error?.code || '') === 'ERR_DLOPEN_FAILED') {
|
|
63
|
+
return [
|
|
64
|
+
`The SQLite binding that better-sqlite3 ships for ${target} does not load on this system (${String(error?.message || '').split('\n')[0]}).`,
|
|
65
|
+
'Its C library is too old for it, or the system is not one the binding supports.',
|
|
66
|
+
'Update the operating system, or run the relay on a supported one.',
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
return [
|
|
70
|
+
`The SQLite binding of better-sqlite3 for ${target} is missing from the installed package (${String(error?.message || '').split('\n')[0]}).`,
|
|
71
|
+
'Reinstall OAR: npm install -g @oar-sh/oar',
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
return [
|
|
75
|
+
`better-sqlite3 has no prebuilt SQLite binding for ${target}, and it has not been built here yet.`,
|
|
76
|
+
`Build it with: ${NATIVE_BUILD_COMMAND}`,
|
|
77
|
+
platform === 'android'
|
|
78
|
+
? `It needs Python 3, make and a C++ compiler: ${TERMUX_BUILD_TOOLS}`
|
|
79
|
+
: 'It needs Python 3, make and a C++ compiler.',
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The node-gyp that ships inside npm, beside this Node; null when there is none.
|
|
84
|
+
function bundledNodeGyp({ execPath, platform, existsImpl, realpathImpl }) {
|
|
85
|
+
const pathImpl = platform === 'win32' ? path.win32 : path.posix;
|
|
86
|
+
const nodeDir = pathImpl.dirname(String(execPath || ''));
|
|
87
|
+
const npmRoots = [];
|
|
88
|
+
if (platform !== 'win32') {
|
|
89
|
+
// <prefix>/bin/npm is a link to <prefix>/lib/node_modules/npm/bin/npm-cli.js.
|
|
90
|
+
try { npmRoots.push(pathImpl.dirname(pathImpl.dirname(realpathImpl(pathImpl.join(nodeDir, 'npm'))))); } catch {}
|
|
91
|
+
npmRoots.push(pathImpl.join(nodeDir, '..', 'lib', 'node_modules', 'npm'));
|
|
92
|
+
} else {
|
|
93
|
+
npmRoots.push(pathImpl.join(nodeDir, 'node_modules', 'npm'));
|
|
94
|
+
}
|
|
95
|
+
for (const root of npmRoots) {
|
|
96
|
+
const file = pathImpl.join(root, 'node_modules', 'node-gyp', 'bin', 'node-gyp.js');
|
|
97
|
+
if (existsImpl(file)) return file;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* How to run node-gyp: npm's own (npm_config_node_gyp when npm set it), else
|
|
104
|
+
* `npx --yes node-gyp`. Returns { command, args, env }.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveNodeGypInvocation({
|
|
107
|
+
execPath = process.execPath,
|
|
108
|
+
platform = process.platform,
|
|
109
|
+
env = process.env,
|
|
110
|
+
existsImpl = fs.existsSync,
|
|
111
|
+
realpathImpl = fs.realpathSync,
|
|
112
|
+
} = {}) {
|
|
113
|
+
const pathImpl = platform === 'win32' ? path.win32 : path.posix;
|
|
114
|
+
const nodeDir = pathImpl.dirname(String(execPath || ''));
|
|
115
|
+
const separator = platform === 'win32' ? ';' : ':';
|
|
116
|
+
const pathKey = Object.keys(env || {}).find((key) => key.toUpperCase() === 'PATH') || 'PATH';
|
|
117
|
+
// node-gyp runs `node` from the gyp files: this Node first on its PATH.
|
|
118
|
+
const childEnv = { ...env, [pathKey]: [nodeDir, String(env?.[pathKey] || '')].filter(Boolean).join(separator) };
|
|
119
|
+
// Node for Termux has no android_ndk_path in its common.gypi, which gyp
|
|
120
|
+
// then refuses as undefined; an empty one builds with Termux's own clang.
|
|
121
|
+
if (platform === 'android' && !/\bandroid_ndk_path=/.test(String(env?.GYP_DEFINES || ''))) {
|
|
122
|
+
childEnv.GYP_DEFINES = [String(env?.GYP_DEFINES || '').trim(), "android_ndk_path=''"].filter(Boolean).join(' ');
|
|
123
|
+
}
|
|
124
|
+
const fromNpm = String(env?.npm_config_node_gyp || '').trim();
|
|
125
|
+
const gyp = (fromNpm && existsImpl(fromNpm) ? fromNpm : null) || bundledNodeGyp({ execPath, platform, existsImpl, realpathImpl });
|
|
126
|
+
if (gyp) return { command: execPath, args: [gyp], env: childEnv };
|
|
127
|
+
if (platform === 'win32') {
|
|
128
|
+
// npx.cmd runs through a shell, which splits a path with a space in it
|
|
129
|
+
// (C:\Program Files): this Node with npm's npx-cli.js, else npx.cmd by name.
|
|
130
|
+
const npxCli = pathImpl.join(nodeDir, 'node_modules', 'npm', 'bin', 'npx-cli.js');
|
|
131
|
+
if (existsImpl(npxCli)) return { command: execPath, args: [npxCli, '--yes', 'node-gyp'], env: childEnv };
|
|
132
|
+
return { command: 'npx.cmd', args: ['--yes', 'node-gyp'], env: childEnv };
|
|
133
|
+
}
|
|
134
|
+
const npx = pathImpl.join(nodeDir, 'npx');
|
|
135
|
+
return { command: existsImpl(npx) ? npx : 'npx', args: ['--yes', 'node-gyp'], env: childEnv };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// What a failed build's output says is missing, as one line to act on.
|
|
139
|
+
function missingToolHint(output, platform) {
|
|
140
|
+
const text = String(output || '');
|
|
141
|
+
const missing = /find Python|Could not find any Python|python3?: (?:not found|command not found)/i.test(text)
|
|
142
|
+
|| /make: (?:not found|command not found)|not found: make|spawn make ENOENT/i.test(text)
|
|
143
|
+
|| /(?:c\+\+|g\+\+|clang\+\+|cc|clang): (?:not found|command not found)|No such file or directory.*(?:c\+\+|g\+\+|clang)/i.test(text);
|
|
144
|
+
if (!missing) return null;
|
|
145
|
+
return platform === 'android'
|
|
146
|
+
? `Install the build tools first: ${TERMUX_BUILD_TOOLS}, then run ${NATIVE_BUILD_COMMAND} again.`
|
|
147
|
+
: `Install Python 3, make and a C++ compiler (build-essential on Debian and Ubuntu), then run ${NATIVE_BUILD_COMMAND} again.`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Opens a database in a child Node with the binding just built, named by its
|
|
151
|
+
// path: the default loader would take a prebuilt one first where one ships.
|
|
152
|
+
function bindingLoads({ dir, execPath, spawnImpl, env }) {
|
|
153
|
+
const binding = path.join(dir, BINDING_FILE);
|
|
154
|
+
const script = `const Database = require(${JSON.stringify(dir)}); new Database(':memory:', { nativeBinding: ${JSON.stringify(binding)} }).close();`;
|
|
155
|
+
return runToCompletion(
|
|
156
|
+
() => spawnImpl(execPath, ['-e', script], { stdio: ['ignore', 'pipe', 'pipe'], env, windowsHide: true }),
|
|
157
|
+
{ timeoutMs: 60_000 },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Builds the binding when this platform has no prebuilt one. `force` builds
|
|
163
|
+
* where one ships too, to check the toolchain; better-sqlite3 still loads its
|
|
164
|
+
* prebuilt binding first there. Resolves (never rejects) with { ok, status, error?, hint? }:
|
|
165
|
+
* status 'prebuilt' (nothing to do), 'built', 'failed'. Every line of the
|
|
166
|
+
* build goes to `onOutput`; what it says about itself goes to `logger.log`.
|
|
167
|
+
*/
|
|
168
|
+
export async function buildBetterSqlite3({
|
|
169
|
+
packageRoot,
|
|
170
|
+
force = false,
|
|
171
|
+
platform = process.platform,
|
|
172
|
+
arch = process.arch,
|
|
173
|
+
execPath = process.execPath,
|
|
174
|
+
env = process.env,
|
|
175
|
+
spawnImpl = defaultSpawn,
|
|
176
|
+
existsImpl = fs.existsSync,
|
|
177
|
+
realpathImpl = fs.realpathSync,
|
|
178
|
+
requireFrom = createRequire,
|
|
179
|
+
timeoutMs = NATIVE_BUILD_TIMEOUT_MS,
|
|
180
|
+
onOutput = null,
|
|
181
|
+
onChild = null,
|
|
182
|
+
logger = console,
|
|
183
|
+
} = {}) {
|
|
184
|
+
if (!force && !needsSourceBuild({ platform, arch })) return { ok: true, status: 'prebuilt' };
|
|
185
|
+
const dir = resolveBetterSqlite3Dir({ packageRoot, requireFrom });
|
|
186
|
+
if (!dir) {
|
|
187
|
+
const error = `better-sqlite3 is not installed under ${packageRoot}`;
|
|
188
|
+
logger.error?.(`[oar] ${error}.`);
|
|
189
|
+
return { ok: false, status: 'failed', error };
|
|
190
|
+
}
|
|
191
|
+
const gyp = resolveNodeGypInvocation({ execPath, platform, env, existsImpl, realpathImpl });
|
|
192
|
+
const args = [...gyp.args, 'rebuild', '--release', '--force_build=1'];
|
|
193
|
+
logger.log?.(`[oar] Building the SQLite binding of better-sqlite3 for ${platform}-${arch} in ${dir}; this takes a few minutes.`);
|
|
194
|
+
const result = await runToCompletion(
|
|
195
|
+
() => spawnImpl(gyp.command, args, {
|
|
196
|
+
cwd: dir,
|
|
197
|
+
env: gyp.env,
|
|
198
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
199
|
+
...(platform === 'win32' ? { windowsHide: true, shell: /\.cmd$/i.test(gyp.command) } : { detached: true }),
|
|
200
|
+
}),
|
|
201
|
+
{
|
|
202
|
+
timeoutMs,
|
|
203
|
+
killChild: (child, signal) => killTree(child, signal, { platform }),
|
|
204
|
+
onChild,
|
|
205
|
+
onOutput,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
if (!result.ok || !existsImpl(path.join(dir, BINDING_FILE))) {
|
|
209
|
+
const error = result.error || `node-gyp exited with code ${result.code}`;
|
|
210
|
+
const hint = missingToolHint(result.output, platform);
|
|
211
|
+
logger.error?.(`[oar] The build of the SQLite binding failed: ${error}.`);
|
|
212
|
+
if (hint) logger.error?.(`[oar] ${hint}`);
|
|
213
|
+
else logger.error?.(`[oar] The end of its output:\n${tailOf(result.output, 2_000)}`);
|
|
214
|
+
return { ok: false, status: 'failed', error, ...(hint ? { hint } : {}) };
|
|
215
|
+
}
|
|
216
|
+
const check = await bindingLoads({ dir, execPath, spawnImpl, env: gyp.env });
|
|
217
|
+
if (!check.ok) {
|
|
218
|
+
const error = `the built binding does not load: ${tailOf(check.output || check.error || '', 500).trim()}`;
|
|
219
|
+
logger.error?.(`[oar] ${error}`);
|
|
220
|
+
return { ok: false, status: 'failed', error };
|
|
221
|
+
}
|
|
222
|
+
logger.log?.('[oar] The SQLite binding is built.');
|
|
223
|
+
return { ok: true, status: 'built' };
|
|
224
|
+
}
|
|
@@ -7,6 +7,16 @@ import { randomBytes } from 'node:crypto';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
AGENT_SDK_IDS,
|
|
12
|
+
agentSdkSupport,
|
|
13
|
+
agentSdkFromCheckout,
|
|
14
|
+
findReadyAgentSdkDir,
|
|
15
|
+
getAgentSdkDescriptor,
|
|
16
|
+
readAgentSdkMarker,
|
|
17
|
+
readAgentSdkPin,
|
|
18
|
+
resolveAgentSdkRoot,
|
|
19
|
+
} from './agent-sdk-paths.mjs';
|
|
10
20
|
import { formatMegabytes } from './db-backup-service.mjs';
|
|
11
21
|
|
|
12
22
|
export function generateAuthToken() {
|
|
@@ -86,6 +96,8 @@ const CLI_COMMANDS = {
|
|
|
86
96
|
autostart: { words: ['signin', 'boot', 'off', 'status'], flags: ['--replace'] },
|
|
87
97
|
uninstall: { flags: ['--purge'] },
|
|
88
98
|
update: { flags: ['--beta'], values: ['--to'] },
|
|
99
|
+
'build-native': { flags: ['--force'] },
|
|
100
|
+
sdk: { words: ['install', 'status'], words2: [...AGENT_SDK_IDS], flags: ['--reinstall'] },
|
|
89
101
|
copilot: { flags: ['--install-extension', '--no-install-extension'], values: ['--port', '--migrate-from'], forwards: true },
|
|
90
102
|
};
|
|
91
103
|
|
|
@@ -116,6 +128,12 @@ export function usageText() {
|
|
|
116
128
|
' oar backup [--reason <text>]',
|
|
117
129
|
' A copy of the database into the backup folder (safe while the relay runs)',
|
|
118
130
|
' oar update [--beta] [--to <version>]',
|
|
131
|
+
' oar sdk [status] The agent SDKs the relay installs on demand: folder, versions, state',
|
|
132
|
+
' oar sdk install claude | cursor | koffi [--reinstall]',
|
|
133
|
+
' Install one now, without the relay (koffi: Windows only)',
|
|
134
|
+
' oar build-native [--force]',
|
|
135
|
+
' Build the SQLite binding where no prebuilt one ships (Termux, 32-bit ARM);',
|
|
136
|
+
' --force builds where one ships too, which only tests the build tools',
|
|
119
137
|
' oar help This text',
|
|
120
138
|
' oar --version',
|
|
121
139
|
].join('\n');
|
|
@@ -176,6 +194,8 @@ export function parseCliArgs(argv = []) {
|
|
|
176
194
|
if (arg === name) index += 1;
|
|
177
195
|
} else if (spec.words?.includes(arg) && words === 0) {
|
|
178
196
|
words += 1;
|
|
197
|
+
} else if (spec.words2?.includes(arg) && words === 1) {
|
|
198
|
+
words += 1;
|
|
179
199
|
} else if (!spec.flags?.includes(arg)) {
|
|
180
200
|
return { error: arg.startsWith('-') ? `Unknown option for oar ${command}: ${arg}` : `Unexpected argument: ${arg}` };
|
|
181
201
|
}
|
|
@@ -398,15 +418,14 @@ export function buildServicePath({ nodeBin, envPath = '', homeDir = '' } = {}) {
|
|
|
398
418
|
|
|
399
419
|
/**
|
|
400
420
|
* The dependencies whose install scripts OAR needs (they fetch or build a
|
|
401
|
-
* native binary). npm 12 runs no install script it was not told to
|
|
402
|
-
* a
|
|
403
|
-
*
|
|
404
|
-
*
|
|
421
|
+
* native binary): none. npm 12 runs no install script it was not told to
|
|
422
|
+
* allow, so a package that gains one must be added here and passed to npm
|
|
423
|
+
* again; `npm-install-scripts.test.mjs` holds this list to the lockfile.
|
|
424
|
+
* better-sqlite3 13 has no install script (where it ships no binary, OAR
|
|
425
|
+
* builds it: native-addon-build-service.mjs), and koffi, whose script the
|
|
426
|
+
* list held, is installed on demand on Windows (agent-sdk-store.mjs).
|
|
405
427
|
*/
|
|
406
|
-
|
|
407
|
-
// kept inside the package before npm deletes it (package-data-rescue.mjs).
|
|
408
|
-
export const NPM_INSTALL_SCRIPT_PACKAGES = Object.freeze(['@oar-sh/oar', 'better-sqlite3', 'koffi']);
|
|
409
|
-
export const NPM_ALLOW_SCRIPTS_ARG = `--allow-scripts=${NPM_INSTALL_SCRIPT_PACKAGES.join(',')}`;
|
|
428
|
+
export const NPM_INSTALL_SCRIPT_PACKAGES = Object.freeze([]);
|
|
410
429
|
|
|
411
430
|
/**
|
|
412
431
|
* How to run `npm install -g` so the update lands in the install that is
|
|
@@ -439,8 +458,9 @@ export function resolveNpmInvocation({
|
|
|
439
458
|
|
|
440
459
|
/**
|
|
441
460
|
* Provider CLIs `oar doctor` probes, with the args that answer fast. Cursor is
|
|
442
|
-
* deliberately absent: the relay drives it through the
|
|
443
|
-
*
|
|
461
|
+
* deliberately absent: the relay drives it through the Cursor SDK it installs
|
|
462
|
+
* on demand (the agent SDK block of the report) and never invokes a
|
|
463
|
+
* cursor-agent binary.
|
|
444
464
|
*/
|
|
445
465
|
export const DOCTOR_PROBES = Object.freeze([
|
|
446
466
|
{ id: 'gh (Copilot)', binary: 'gh', args: ['--version'] },
|
|
@@ -484,6 +504,50 @@ function renderBackupLines(backups, now) {
|
|
|
484
504
|
return lines;
|
|
485
505
|
}
|
|
486
506
|
|
|
507
|
+
/**
|
|
508
|
+
* The agent SDKs as `oar doctor` and `oar sdk` show them, read from the disk
|
|
509
|
+
* alone (no lock, no spawn): the folder, and per SDK the pinned version, an
|
|
510
|
+
* installed one and its state. A checkout runs its devDependencies instead.
|
|
511
|
+
*/
|
|
512
|
+
export function describeAgentSdks({ env = process.env, platform = process.platform } = {}) {
|
|
513
|
+
// A checkout takes each SDK from its node_modules while it is installed there.
|
|
514
|
+
const fromCheckout = AGENT_SDK_IDS.every((id) => agentSdkFromCheckout(id, { env }) || getAgentSdkDescriptor(id).platforms?.includes(platform) === false);
|
|
515
|
+
const root = resolveAgentSdkRoot({ env, platform });
|
|
516
|
+
const rows = [];
|
|
517
|
+
for (const id of AGENT_SDK_IDS) {
|
|
518
|
+
const descriptor = getAgentSdkDescriptor(id);
|
|
519
|
+
let pin = null;
|
|
520
|
+
try { pin = readAgentSdkPin(id); } catch {}
|
|
521
|
+
if (!pin) {
|
|
522
|
+
rows.push({ id, label: descriptor.label, pinned: null, installed: null, state: 'pins missing from the package' });
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const support = agentSdkSupport(pin);
|
|
526
|
+
if (descriptor.platforms && !descriptor.platforms.includes(platform)) continue;
|
|
527
|
+
if (agentSdkFromCheckout(id, { env })) {
|
|
528
|
+
rows.push({ id, label: descriptor.label, pinned: pin.version, installed: null, state: 'from the checkout (devDependency)' });
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const ready = findReadyAgentSdkDir(id, { env, platform, root });
|
|
532
|
+
const older = ready ? null : findReadyAgentSdkDir(id, { env, platform, root, anyVersion: true });
|
|
533
|
+
const olderVersion = older ? readAgentSdkMarker(older)?.version || null : null;
|
|
534
|
+
const state = support === 'no'
|
|
535
|
+
? 'not available on this platform'
|
|
536
|
+
: ready ? `ready${support === 'cli' ? " (runs the host's Claude Code CLI)" : ''}`
|
|
537
|
+
: olderVersion ? `${olderVersion} installed; the relay installs ${pin.version} at its next start or first chat`
|
|
538
|
+
: 'not installed (installs on first setup or first chat)';
|
|
539
|
+
rows.push({ id, label: descriptor.label, pinned: pin.version, installed: ready ? pin.version : olderVersion, state, dir: ready || older || null });
|
|
540
|
+
}
|
|
541
|
+
return { fromCheckout, root, rows };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function renderAgentSdkLines(agentSdks) {
|
|
545
|
+
if (!agentSdks) return [];
|
|
546
|
+
const lines = [` agent SDKs : ${agentSdks.fromCheckout ? "from the checkout's node_modules (OAR_AGENT_SDK_SOURCE=store uses the store)" : agentSdks.root}`];
|
|
547
|
+
for (const row of agentSdks.rows) lines.push(` ${row.id.padEnd(14)}: ${row.pinned ? `${row.pinned} ` : ''}${row.state}`);
|
|
548
|
+
return lines;
|
|
549
|
+
}
|
|
550
|
+
|
|
487
551
|
export function renderDoctorReport({
|
|
488
552
|
version,
|
|
489
553
|
nodeVersion,
|
|
@@ -499,6 +563,7 @@ export function renderDoctorReport({
|
|
|
499
563
|
probes = [],
|
|
500
564
|
warnings = [],
|
|
501
565
|
tunnel = '',
|
|
566
|
+
agentSdks = null,
|
|
502
567
|
}) {
|
|
503
568
|
const yesNo = (v) => (v ? 'yes' : 'no');
|
|
504
569
|
const lines = [
|
|
@@ -524,6 +589,7 @@ export function renderDoctorReport({
|
|
|
524
589
|
for (const probe of probes) {
|
|
525
590
|
lines.push(` ${probe.id.padEnd(14)}: ${probe.ok ? probe.version || 'installed' : 'not found'}`);
|
|
526
591
|
}
|
|
592
|
+
lines.push(...renderAgentSdkLines(agentSdks));
|
|
527
593
|
for (const warning of warnings) lines.push(` warning : ${warning}`);
|
|
528
594
|
return lines.join('\n');
|
|
529
595
|
}
|
|
@@ -21,25 +21,15 @@
|
|
|
21
21
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
|
-
import os from 'node:os';
|
|
25
24
|
|
|
26
25
|
import { DB_FILE_NAMES, LEGACY_DB_FILE_NAME, resolveDatabaseFile, sideFiles } from './database-file.mjs';
|
|
26
|
+
import { resolveOarRoot } from './oar-state-root.mjs';
|
|
27
27
|
import { createRelaySingletonGuard } from './relay-singleton-guard.mjs';
|
|
28
28
|
|
|
29
29
|
export const MIGRATION_MARKER = '.migrated';
|
|
30
30
|
const MAX_LOGS_BYTES = 50 * 1024 * 1024;
|
|
31
31
|
|
|
32
|
-
export
|
|
33
|
-
const override = String(env.OAR_STATE_ROOT || '').trim();
|
|
34
|
-
if (override) return path.resolve(override);
|
|
35
|
-
if (platform === 'win32') {
|
|
36
|
-
const appData = String(env.APPDATA || '').trim();
|
|
37
|
-
if (appData) return path.join(appData, 'oar');
|
|
38
|
-
const profile = String(env.USERPROFILE || '').trim();
|
|
39
|
-
return path.join(profile || os.homedir(), 'AppData', 'Roaming', 'oar');
|
|
40
|
-
}
|
|
41
|
-
return path.join(os.homedir(), '.oar');
|
|
42
|
-
}
|
|
32
|
+
export { resolveOarRoot };
|
|
43
33
|
|
|
44
34
|
function defaultIsProcessAlive(pid) {
|
|
45
35
|
try {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// The OAR state root: ~/.oar, %APPDATA%\oar on Windows, OAR_STATE_ROOT when
|
|
5
|
+
// set. A module of its own, without imports of OAR's, so the relay's lock
|
|
6
|
+
// check can find the agent SDK folder beside it (agent-sdk-paths.mjs).
|
|
7
|
+
export function resolveOarRoot(env = process.env, platform = process.platform) {
|
|
8
|
+
const override = String(env.OAR_STATE_ROOT || '').trim();
|
|
9
|
+
if (override) return path.resolve(override);
|
|
10
|
+
if (platform === 'win32') {
|
|
11
|
+
const appData = String(env.APPDATA || '').trim();
|
|
12
|
+
if (appData) return path.join(appData, 'oar');
|
|
13
|
+
const profile = String(env.USERPROFILE || '').trim();
|
|
14
|
+
return path.join(profile || os.homedir(), 'AppData', 'Roaming', 'oar');
|
|
15
|
+
}
|
|
16
|
+
return path.join(os.homedir(), '.oar');
|
|
17
|
+
}
|