@arcote.tech/arc-cli 0.7.7 → 0.7.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/dist/index.js +152 -51
- package/package.json +9 -9
- package/src/builder/dependency-collector.ts +16 -0
- package/src/builder/module-builder.ts +5 -2
- package/src/deploy/compose.ts +9 -2
- package/src/deploy/config.ts +19 -1
- package/src/deploy/observability-configs.ts +11 -6
- package/src/platform/server.ts +14 -2
- package/src/platform/startup.ts +32 -10
package/dist/index.js
CHANGED
|
@@ -14860,10 +14860,11 @@ class ArcFunction {
|
|
|
14860
14860
|
params: schema instanceof ArcObject ? schema : new ArcObject(schema)
|
|
14861
14861
|
});
|
|
14862
14862
|
}
|
|
14863
|
-
withResult(
|
|
14863
|
+
withResult(...schemas) {
|
|
14864
|
+
const results = schemas.map((s) => s instanceof ArcObject ? s : new ArcObject(s));
|
|
14864
14865
|
return new ArcFunction({
|
|
14865
14866
|
...this.data,
|
|
14866
|
-
|
|
14867
|
+
results
|
|
14867
14868
|
});
|
|
14868
14869
|
}
|
|
14869
14870
|
query(elements) {
|
|
@@ -14891,6 +14892,12 @@ class ArcFunction {
|
|
|
14891
14892
|
description: desc
|
|
14892
14893
|
});
|
|
14893
14894
|
}
|
|
14895
|
+
private() {
|
|
14896
|
+
return new ArcFunction({
|
|
14897
|
+
...this.data,
|
|
14898
|
+
isPrivate: true
|
|
14899
|
+
});
|
|
14900
|
+
}
|
|
14894
14901
|
handle(handler) {
|
|
14895
14902
|
return new ArcFunction({
|
|
14896
14903
|
...this.data,
|
|
@@ -14912,8 +14919,8 @@ class ArcFunction {
|
|
|
14912
14919
|
get params() {
|
|
14913
14920
|
return this.data.params;
|
|
14914
14921
|
}
|
|
14915
|
-
get
|
|
14916
|
-
return this.data.
|
|
14922
|
+
get results() {
|
|
14923
|
+
return this.data.results;
|
|
14917
14924
|
}
|
|
14918
14925
|
async verifyProtections(tokens) {
|
|
14919
14926
|
if (!this.data.protections || this.data.protections.length === 0) {
|
|
@@ -14937,7 +14944,7 @@ class ArcFunction {
|
|
|
14937
14944
|
toJsonSchema() {
|
|
14938
14945
|
return {
|
|
14939
14946
|
params: this.data.params?.toJsonSchema?.() ?? null,
|
|
14940
|
-
|
|
14947
|
+
results: this.data.results.map((r2) => r2.toJsonSchema())
|
|
14941
14948
|
};
|
|
14942
14949
|
}
|
|
14943
14950
|
}
|
|
@@ -15294,19 +15301,26 @@ function buildContextAccessor(context2, adapters, contextMethod, onCall) {
|
|
|
15294
15301
|
if (typeof ctxFn !== "function")
|
|
15295
15302
|
continue;
|
|
15296
15303
|
const methods = ctxFn.call(element2, adapters);
|
|
15297
|
-
if (!methods
|
|
15304
|
+
if (!methods)
|
|
15305
|
+
continue;
|
|
15306
|
+
const elementName = element2.name;
|
|
15307
|
+
if (typeof methods === "function") {
|
|
15308
|
+
result[elementName] = (...args) => onCall({ element: elementName, method: "", args }, () => methods(...args));
|
|
15309
|
+
continue;
|
|
15310
|
+
}
|
|
15311
|
+
if (typeof methods !== "object")
|
|
15298
15312
|
continue;
|
|
15299
15313
|
const wrapped = {};
|
|
15300
15314
|
for (const [methodName, method] of Object.entries(methods)) {
|
|
15301
15315
|
if (typeof method !== "function")
|
|
15302
15316
|
continue;
|
|
15303
|
-
wrapped[methodName] = (...args) => onCall({ element:
|
|
15317
|
+
wrapped[methodName] = (...args) => onCall({ element: elementName, method: methodName, args }, () => method(...args));
|
|
15304
15318
|
}
|
|
15305
|
-
result[
|
|
15319
|
+
result[elementName] = wrapped;
|
|
15306
15320
|
}
|
|
15307
15321
|
return result;
|
|
15308
15322
|
}
|
|
15309
|
-
function executeDescriptor(descriptor, context2, adapters, contextMethod) {
|
|
15323
|
+
function executeDescriptor(descriptor, context2, adapters, contextMethod, options) {
|
|
15310
15324
|
const element2 = context2.get(descriptor.element);
|
|
15311
15325
|
if (!element2) {
|
|
15312
15326
|
throw new Error(`Element '${descriptor.element}' not found in context`);
|
|
@@ -15316,10 +15330,19 @@ function executeDescriptor(descriptor, context2, adapters, contextMethod) {
|
|
|
15316
15330
|
throw new Error(`Element '${descriptor.element}' has no ${contextMethod}`);
|
|
15317
15331
|
}
|
|
15318
15332
|
const methods = ctxFn.call(element2, adapters);
|
|
15333
|
+
if (typeof methods === "function") {
|
|
15334
|
+
if (options?.fromWire && methods.__isPrivate) {
|
|
15335
|
+
throw new Error(`Element "${descriptor.element}" is private and not callable from a client.`);
|
|
15336
|
+
}
|
|
15337
|
+
return methods(...descriptor.args);
|
|
15338
|
+
}
|
|
15319
15339
|
const method = methods?.[descriptor.method];
|
|
15320
15340
|
if (typeof method !== "function") {
|
|
15321
15341
|
throw new Error(`Method '${descriptor.method}' not found on '${descriptor.element}'`);
|
|
15322
15342
|
}
|
|
15343
|
+
if (options?.fromWire && method.__isPrivate) {
|
|
15344
|
+
throw new Error(`Method "${descriptor.element}.${descriptor.method}" is private and not callable from a client.`);
|
|
15345
|
+
}
|
|
15323
15346
|
return method(...descriptor.args);
|
|
15324
15347
|
}
|
|
15325
15348
|
|
|
@@ -19202,7 +19225,7 @@ var init_dist = __esm(() => {
|
|
|
19202
19225
|
this.data = data;
|
|
19203
19226
|
this.#fn = fn ?? new ArcFunction({
|
|
19204
19227
|
params: data.params,
|
|
19205
|
-
|
|
19228
|
+
results: data.results,
|
|
19206
19229
|
queryElements: data.queryElements,
|
|
19207
19230
|
mutationElements: data.mutationElements,
|
|
19208
19231
|
protections: data.protections || [],
|
|
@@ -19233,7 +19256,8 @@ var init_dist = __esm(() => {
|
|
|
19233
19256
|
}, newFn);
|
|
19234
19257
|
}
|
|
19235
19258
|
withResult(...schemas) {
|
|
19236
|
-
|
|
19259
|
+
const newFn = this.#fn.withResult(...schemas);
|
|
19260
|
+
return new ArcCommand({ ...this.data, results: newFn.data.results }, newFn);
|
|
19237
19261
|
}
|
|
19238
19262
|
handle(handler) {
|
|
19239
19263
|
const newFn = new ArcFunction({
|
|
@@ -19324,7 +19348,7 @@ var init_dist = __esm(() => {
|
|
|
19324
19348
|
this.data = data;
|
|
19325
19349
|
this.#fn = fn ?? new ArcFunction({
|
|
19326
19350
|
params: null,
|
|
19327
|
-
|
|
19351
|
+
results: [],
|
|
19328
19352
|
queryElements: data.queryElements,
|
|
19329
19353
|
mutationElements: data.mutationElements,
|
|
19330
19354
|
protections: [],
|
|
@@ -19430,7 +19454,7 @@ var init_dist = __esm(() => {
|
|
|
19430
19454
|
this.data = data;
|
|
19431
19455
|
this.#fn = fn ?? new ArcFunction({
|
|
19432
19456
|
params: null,
|
|
19433
|
-
|
|
19457
|
+
results: [],
|
|
19434
19458
|
queryElements: data.queryElements,
|
|
19435
19459
|
mutationElements: data.mutationElements,
|
|
19436
19460
|
protections: data.protections || [],
|
|
@@ -22193,10 +22217,11 @@ class ArcFunction2 {
|
|
|
22193
22217
|
params: schema instanceof ArcObject2 ? schema : new ArcObject2(schema)
|
|
22194
22218
|
});
|
|
22195
22219
|
}
|
|
22196
|
-
withResult(
|
|
22220
|
+
withResult(...schemas) {
|
|
22221
|
+
const results = schemas.map((s) => s instanceof ArcObject2 ? s : new ArcObject2(s));
|
|
22197
22222
|
return new ArcFunction2({
|
|
22198
22223
|
...this.data,
|
|
22199
|
-
|
|
22224
|
+
results
|
|
22200
22225
|
});
|
|
22201
22226
|
}
|
|
22202
22227
|
query(elements) {
|
|
@@ -22224,6 +22249,12 @@ class ArcFunction2 {
|
|
|
22224
22249
|
description: desc
|
|
22225
22250
|
});
|
|
22226
22251
|
}
|
|
22252
|
+
private() {
|
|
22253
|
+
return new ArcFunction2({
|
|
22254
|
+
...this.data,
|
|
22255
|
+
isPrivate: true
|
|
22256
|
+
});
|
|
22257
|
+
}
|
|
22227
22258
|
handle(handler) {
|
|
22228
22259
|
return new ArcFunction2({
|
|
22229
22260
|
...this.data,
|
|
@@ -22245,8 +22276,8 @@ class ArcFunction2 {
|
|
|
22245
22276
|
get params() {
|
|
22246
22277
|
return this.data.params;
|
|
22247
22278
|
}
|
|
22248
|
-
get
|
|
22249
|
-
return this.data.
|
|
22279
|
+
get results() {
|
|
22280
|
+
return this.data.results;
|
|
22250
22281
|
}
|
|
22251
22282
|
async verifyProtections(tokens) {
|
|
22252
22283
|
if (!this.data.protections || this.data.protections.length === 0) {
|
|
@@ -22270,7 +22301,7 @@ class ArcFunction2 {
|
|
|
22270
22301
|
toJsonSchema() {
|
|
22271
22302
|
return {
|
|
22272
22303
|
params: this.data.params?.toJsonSchema?.() ?? null,
|
|
22273
|
-
|
|
22304
|
+
results: this.data.results.map((r2) => r2.toJsonSchema())
|
|
22274
22305
|
};
|
|
22275
22306
|
}
|
|
22276
22307
|
}
|
|
@@ -22627,19 +22658,26 @@ function buildContextAccessor2(context2, adapters, contextMethod, onCall) {
|
|
|
22627
22658
|
if (typeof ctxFn !== "function")
|
|
22628
22659
|
continue;
|
|
22629
22660
|
const methods = ctxFn.call(element2, adapters);
|
|
22630
|
-
if (!methods
|
|
22661
|
+
if (!methods)
|
|
22662
|
+
continue;
|
|
22663
|
+
const elementName = element2.name;
|
|
22664
|
+
if (typeof methods === "function") {
|
|
22665
|
+
result[elementName] = (...args) => onCall({ element: elementName, method: "", args }, () => methods(...args));
|
|
22666
|
+
continue;
|
|
22667
|
+
}
|
|
22668
|
+
if (typeof methods !== "object")
|
|
22631
22669
|
continue;
|
|
22632
22670
|
const wrapped = {};
|
|
22633
22671
|
for (const [methodName, method] of Object.entries(methods)) {
|
|
22634
22672
|
if (typeof method !== "function")
|
|
22635
22673
|
continue;
|
|
22636
|
-
wrapped[methodName] = (...args) => onCall({ element:
|
|
22674
|
+
wrapped[methodName] = (...args) => onCall({ element: elementName, method: methodName, args }, () => method(...args));
|
|
22637
22675
|
}
|
|
22638
|
-
result[
|
|
22676
|
+
result[elementName] = wrapped;
|
|
22639
22677
|
}
|
|
22640
22678
|
return result;
|
|
22641
22679
|
}
|
|
22642
|
-
function executeDescriptor2(descriptor, context2, adapters, contextMethod) {
|
|
22680
|
+
function executeDescriptor2(descriptor, context2, adapters, contextMethod, options) {
|
|
22643
22681
|
const element2 = context2.get(descriptor.element);
|
|
22644
22682
|
if (!element2) {
|
|
22645
22683
|
throw new Error(`Element '${descriptor.element}' not found in context`);
|
|
@@ -22649,10 +22687,19 @@ function executeDescriptor2(descriptor, context2, adapters, contextMethod) {
|
|
|
22649
22687
|
throw new Error(`Element '${descriptor.element}' has no ${contextMethod}`);
|
|
22650
22688
|
}
|
|
22651
22689
|
const methods = ctxFn.call(element2, adapters);
|
|
22690
|
+
if (typeof methods === "function") {
|
|
22691
|
+
if (options?.fromWire && methods.__isPrivate) {
|
|
22692
|
+
throw new Error(`Element "${descriptor.element}" is private and not callable from a client.`);
|
|
22693
|
+
}
|
|
22694
|
+
return methods(...descriptor.args);
|
|
22695
|
+
}
|
|
22652
22696
|
const method = methods?.[descriptor.method];
|
|
22653
22697
|
if (typeof method !== "function") {
|
|
22654
22698
|
throw new Error(`Method '${descriptor.method}' not found on '${descriptor.element}'`);
|
|
22655
22699
|
}
|
|
22700
|
+
if (options?.fromWire && method.__isPrivate) {
|
|
22701
|
+
throw new Error(`Method "${descriptor.element}.${descriptor.method}" is private and not callable from a client.`);
|
|
22702
|
+
}
|
|
22656
22703
|
return method(...descriptor.args);
|
|
22657
22704
|
}
|
|
22658
22705
|
|
|
@@ -24970,7 +25017,7 @@ var init_dist2 = __esm(() => {
|
|
|
24970
25017
|
this.data = data;
|
|
24971
25018
|
this.#fn = fn ?? new ArcFunction2({
|
|
24972
25019
|
params: data.params,
|
|
24973
|
-
|
|
25020
|
+
results: data.results,
|
|
24974
25021
|
queryElements: data.queryElements,
|
|
24975
25022
|
mutationElements: data.mutationElements,
|
|
24976
25023
|
protections: data.protections || [],
|
|
@@ -25001,7 +25048,8 @@ var init_dist2 = __esm(() => {
|
|
|
25001
25048
|
}, newFn);
|
|
25002
25049
|
}
|
|
25003
25050
|
withResult(...schemas) {
|
|
25004
|
-
|
|
25051
|
+
const newFn = this.#fn.withResult(...schemas);
|
|
25052
|
+
return new ArcCommand2({ ...this.data, results: newFn.data.results }, newFn);
|
|
25005
25053
|
}
|
|
25006
25054
|
handle(handler) {
|
|
25007
25055
|
const newFn = new ArcFunction2({
|
|
@@ -25092,7 +25140,7 @@ var init_dist2 = __esm(() => {
|
|
|
25092
25140
|
this.data = data;
|
|
25093
25141
|
this.#fn = fn ?? new ArcFunction2({
|
|
25094
25142
|
params: null,
|
|
25095
|
-
|
|
25143
|
+
results: [],
|
|
25096
25144
|
queryElements: data.queryElements,
|
|
25097
25145
|
mutationElements: data.mutationElements,
|
|
25098
25146
|
protections: [],
|
|
@@ -25198,7 +25246,7 @@ var init_dist2 = __esm(() => {
|
|
|
25198
25246
|
this.data = data;
|
|
25199
25247
|
this.#fn = fn ?? new ArcFunction2({
|
|
25200
25248
|
params: null,
|
|
25201
|
-
|
|
25249
|
+
results: [],
|
|
25202
25250
|
queryElements: data.queryElements,
|
|
25203
25251
|
mutationElements: data.mutationElements,
|
|
25204
25252
|
protections: data.protections || [],
|
|
@@ -25770,6 +25818,7 @@ __export(exports_init_server, {
|
|
|
25770
25818
|
wrapDbAdapter: () => wrapDbAdapter,
|
|
25771
25819
|
initServerTelemetry: () => initServerTelemetry
|
|
25772
25820
|
});
|
|
25821
|
+
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
25773
25822
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
25774
25823
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
25775
25824
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
@@ -26083,6 +26132,9 @@ function wrapDbAdapter(adapter, telemetry, dbSystem) {
|
|
|
26083
26132
|
}
|
|
26084
26133
|
function initServerTelemetry(config) {
|
|
26085
26134
|
const telemetry = new ArcTelemetry({ ...config, environment: "server" });
|
|
26135
|
+
if (telemetry.config.debug) {
|
|
26136
|
+
diag.setLogger(new DiagConsoleLogger, DiagLogLevel.ALL);
|
|
26137
|
+
}
|
|
26086
26138
|
if (!telemetry.config.enabled) {
|
|
26087
26139
|
return { telemetry, shutdown: async () => {} };
|
|
26088
26140
|
}
|
|
@@ -26131,7 +26183,8 @@ function initServerTelemetry(config) {
|
|
|
26131
26183
|
serviceName: config.serviceName,
|
|
26132
26184
|
endpoint,
|
|
26133
26185
|
sampleRate: telemetry.config.sampleRate,
|
|
26134
|
-
mode: telemetry.config.mode
|
|
26186
|
+
mode: telemetry.config.mode,
|
|
26187
|
+
active: telemetry.active
|
|
26135
26188
|
});
|
|
26136
26189
|
}
|
|
26137
26190
|
const shutdown = async () => {
|
|
@@ -34965,8 +35018,11 @@ var TAILWIND_INPUT_TEMPLATE = (rootRel) => `@import "tailwindcss";
|
|
|
34965
35018
|
|
|
34966
35019
|
@source "${rootRel}/packages/*/*.{ts,tsx}";
|
|
34967
35020
|
@source "${rootRel}/packages/*/src/**/*.{ts,tsx}";
|
|
34968
|
-
|
|
34969
|
-
|
|
35021
|
+
/* Skanuj wszystkie framework packages \u2014 fragmenty (arc-chat, arc-ai-voice\u2026)
|
|
35022
|
+
wnosz\u0105 w\u0142asne komponenty React z klasami Tailwind, kt\xF3re musz\u0105 trafi\u0107 do
|
|
35023
|
+
CSS. Bez tego absolute positioning, color/border klasy w nowych fragmentach
|
|
35024
|
+
s\u0105 ignorowane \u2014 komponent renderuje si\u0119 bez styl\xF3w. */
|
|
35025
|
+
@source "${rootRel}/node_modules/@arcote.tech/*/src/**/*.{ts,tsx}";
|
|
34970
35026
|
|
|
34971
35027
|
@custom-variant dark (&:is(.dark *));
|
|
34972
35028
|
|
|
@@ -35233,11 +35289,21 @@ function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = []) {
|
|
|
35233
35289
|
} catch (e) {
|
|
35234
35290
|
console.warn(`[arc-otel] could not resolve @arcote.tech/arc-otel \u2014 image will run without telemetry deps: ${e.message}`);
|
|
35235
35291
|
}
|
|
35292
|
+
let rootArc;
|
|
35293
|
+
const rootPkgPath = join10(rootDir, "package.json");
|
|
35294
|
+
if (existsSync9(rootPkgPath)) {
|
|
35295
|
+
try {
|
|
35296
|
+
const rootPkg = JSON.parse(readFileSync9(rootPkgPath, "utf-8"));
|
|
35297
|
+
if (rootPkg.arc && typeof rootPkg.arc === "object")
|
|
35298
|
+
rootArc = rootPkg.arc;
|
|
35299
|
+
} catch {}
|
|
35300
|
+
}
|
|
35236
35301
|
const manifest = {
|
|
35237
35302
|
name: "arc-platform-framework",
|
|
35238
35303
|
private: true,
|
|
35239
35304
|
type: "module",
|
|
35240
|
-
dependencies: versions
|
|
35305
|
+
dependencies: versions,
|
|
35306
|
+
...rootArc ? { arc: rootArc } : {}
|
|
35241
35307
|
};
|
|
35242
35308
|
const manifestPath = join10(arcDir, "package.json");
|
|
35243
35309
|
writeFileSync8(manifestPath, JSON.stringify(manifest, null, 2) + `
|
|
@@ -36066,6 +36132,10 @@ function generateCompose({ cfg }) {
|
|
|
36066
36132
|
lines.push(" retries: 20");
|
|
36067
36133
|
lines.push(" networks:");
|
|
36068
36134
|
lines.push(" - arc-net");
|
|
36135
|
+
if (env2.db.exposeOnLoopback !== undefined) {
|
|
36136
|
+
lines.push(" ports:");
|
|
36137
|
+
lines.push(` - "127.0.0.1:${env2.db.exposeOnLoopback}:5432"`);
|
|
36138
|
+
}
|
|
36069
36139
|
lines.push("");
|
|
36070
36140
|
}
|
|
36071
36141
|
if (cfg.observability?.enabled) {
|
|
@@ -36255,7 +36325,10 @@ processors:
|
|
|
36255
36325
|
send_batch_max_size: 1024
|
|
36256
36326
|
|
|
36257
36327
|
# Tail-based sampling \u2014 applied after a full trace has been assembled.
|
|
36258
|
-
# Errors
|
|
36328
|
+
# Errors + slow traces zachowywane w 100%, normalne traces r\xF3wnie\u017C 100%
|
|
36329
|
+
# przy obecnej skali (boostrap produkcji). Tail sampling matchuje OR po
|
|
36330
|
+
# policies \u2014 bez "always" policy WSZYSTKIE OK traces by\u0142yby droppowane.
|
|
36331
|
+
# Obni\u017C 'random_100pct' do np. 10% gdy ruch eksploduje.
|
|
36259
36332
|
tail_sampling:
|
|
36260
36333
|
decision_wait: 10s
|
|
36261
36334
|
num_traces: 50000
|
|
@@ -36267,9 +36340,9 @@ processors:
|
|
|
36267
36340
|
- name: slow
|
|
36268
36341
|
type: latency
|
|
36269
36342
|
latency: { threshold_ms: 500 }
|
|
36270
|
-
- name:
|
|
36343
|
+
- name: random_100pct
|
|
36271
36344
|
type: probabilistic
|
|
36272
|
-
probabilistic: { sampling_percentage:
|
|
36345
|
+
probabilistic: { sampling_percentage: 100 }
|
|
36273
36346
|
|
|
36274
36347
|
# Drop high-cardinality / PII attributes that might slip past app-side
|
|
36275
36348
|
# sanitization. Belt-and-suspenders before they hit long-term storage.
|
|
@@ -37188,7 +37261,8 @@ function validateDeployConfig(input) {
|
|
|
37188
37261
|
} else if (dbType === "postgres") {
|
|
37189
37262
|
db = {
|
|
37190
37263
|
type: "postgres",
|
|
37191
|
-
image: optionalString(dbRaw, `envs.${name}.db.image`)
|
|
37264
|
+
image: optionalString(dbRaw, `envs.${name}.db.image`),
|
|
37265
|
+
exposeOnLoopback: optionalNumber(dbRaw, `envs.${name}.db.exposeOnLoopback`)
|
|
37192
37266
|
};
|
|
37193
37267
|
} else {
|
|
37194
37268
|
throw new Error(`deploy.arc.json: envs.${name}.db.type must be "sqlite" or "postgres" (got "${dbType}")`);
|
|
@@ -39069,7 +39143,7 @@ class ContextHandler {
|
|
|
39069
39143
|
console.log(`[ARC:Seed] Seeded ${data.length} row(s) into ${tableName}`);
|
|
39070
39144
|
}
|
|
39071
39145
|
}
|
|
39072
|
-
async executeCommand(commandName, params, rawToken) {
|
|
39146
|
+
async executeCommand(commandName, params, rawToken, options) {
|
|
39073
39147
|
const includePayloads = this.telemetry?.shouldIncludePayloads() ?? false;
|
|
39074
39148
|
const baseAttrs = {
|
|
39075
39149
|
"rpc.system": "arc",
|
|
@@ -39094,6 +39168,9 @@ class ContextHandler {
|
|
|
39094
39168
|
if (!command) {
|
|
39095
39169
|
throw new Error(`Command '${commandName}' not found`);
|
|
39096
39170
|
}
|
|
39171
|
+
if (!options?.internal && command.__isPrivate) {
|
|
39172
|
+
throw new Error(`Command '${commandName}' is private and not callable from a client.`);
|
|
39173
|
+
}
|
|
39097
39174
|
try {
|
|
39098
39175
|
return await command(params);
|
|
39099
39176
|
} catch (error) {
|
|
@@ -39701,7 +39778,7 @@ class CronScheduler {
|
|
|
39701
39778
|
console.log(`[ARC:Cron] ${commandName}: executing for ${instances.length} instance(s)...`);
|
|
39702
39779
|
for (const instance of instances) {
|
|
39703
39780
|
try {
|
|
39704
|
-
await this.contextHandler.executeCommand(commandName, { _id: instance._id }, null);
|
|
39781
|
+
await this.contextHandler.executeCommand(commandName, { _id: instance._id }, null, { internal: true });
|
|
39705
39782
|
} catch (error) {
|
|
39706
39783
|
console.error(`[ARC:Cron] ${commandName} failed for instance ${instance._id}:`, error);
|
|
39707
39784
|
}
|
|
@@ -40585,7 +40662,7 @@ function staticFilesHandler(ws, devMode, getManifest) {
|
|
|
40585
40662
|
return null;
|
|
40586
40663
|
};
|
|
40587
40664
|
}
|
|
40588
|
-
function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap) {
|
|
40665
|
+
function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap, telemetry) {
|
|
40589
40666
|
return (req, url, ctx) => {
|
|
40590
40667
|
if (url.pathname === "/api/modules") {
|
|
40591
40668
|
const arcTokensHeader = req.headers.get("X-Arc-Tokens");
|
|
@@ -40605,7 +40682,11 @@ function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap) {
|
|
|
40605
40682
|
return Response.json({
|
|
40606
40683
|
status: "ok",
|
|
40607
40684
|
groups: Object.keys(getManifest().groups).length,
|
|
40608
|
-
clients: cm?.clientCount ?? 0
|
|
40685
|
+
clients: cm?.clientCount ?? 0,
|
|
40686
|
+
otel: {
|
|
40687
|
+
enabled: telemetry?.config.enabled ?? false,
|
|
40688
|
+
active: telemetry?.active ?? false
|
|
40689
|
+
}
|
|
40609
40690
|
}, { headers: ctx.corsHeaders });
|
|
40610
40691
|
}
|
|
40611
40692
|
return null;
|
|
@@ -40656,7 +40737,8 @@ async function startPlatformServer(opts) {
|
|
|
40656
40737
|
environment: "server",
|
|
40657
40738
|
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
40658
40739
|
mode: devMode ? "development" : "production",
|
|
40659
|
-
sampleRate: devMode ? 1 : 1
|
|
40740
|
+
sampleRate: devMode ? 1 : 1,
|
|
40741
|
+
debug: process.env.ARC_OTEL_DEBUG === "true"
|
|
40660
40742
|
});
|
|
40661
40743
|
telemetry = init2.telemetry;
|
|
40662
40744
|
telemetryShutdown = init2.shutdown;
|
|
@@ -40706,7 +40788,7 @@ async function startPlatformServer(opts) {
|
|
|
40706
40788
|
corsHeaders: cors
|
|
40707
40789
|
};
|
|
40708
40790
|
const handlers = [
|
|
40709
|
-
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
40791
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry),
|
|
40710
40792
|
devReloadHandler(sseClients),
|
|
40711
40793
|
staticFilesHandler(ws, !!devMode, getManifest),
|
|
40712
40794
|
spaFallbackHandler(getShellHtml)
|
|
@@ -40744,7 +40826,7 @@ async function startPlatformServer(opts) {
|
|
|
40744
40826
|
dbAdapterFactory,
|
|
40745
40827
|
port,
|
|
40746
40828
|
httpHandlers: [
|
|
40747
|
-
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
40829
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry),
|
|
40748
40830
|
devReloadHandler(sseClients),
|
|
40749
40831
|
staticFilesHandler(ws, !!devMode, getManifest),
|
|
40750
40832
|
spaFallbackHandler(getShellHtml)
|
|
@@ -40788,16 +40870,35 @@ async function startPlatform(opts) {
|
|
|
40788
40870
|
} else {
|
|
40789
40871
|
log2("No context \u2014 server endpoints skipped");
|
|
40790
40872
|
}
|
|
40791
|
-
const
|
|
40792
|
-
|
|
40793
|
-
|
|
40794
|
-
|
|
40795
|
-
|
|
40796
|
-
|
|
40797
|
-
|
|
40798
|
-
|
|
40799
|
-
|
|
40800
|
-
|
|
40873
|
+
const MAX_PORT_ATTEMPTS = devMode ? 20 : 1;
|
|
40874
|
+
let platform3 = null;
|
|
40875
|
+
let actualPort = port;
|
|
40876
|
+
for (let i = 0;i < MAX_PORT_ATTEMPTS; i++) {
|
|
40877
|
+
try {
|
|
40878
|
+
platform3 = await startPlatformServer({
|
|
40879
|
+
ws,
|
|
40880
|
+
port: actualPort,
|
|
40881
|
+
manifest,
|
|
40882
|
+
context: context2,
|
|
40883
|
+
moduleAccess,
|
|
40884
|
+
dbPath,
|
|
40885
|
+
devMode
|
|
40886
|
+
});
|
|
40887
|
+
break;
|
|
40888
|
+
} catch (e2) {
|
|
40889
|
+
const msg = e2.message || "";
|
|
40890
|
+
const inUse = msg.includes("EADDRINUSE") || msg.includes("address already in use") || msg.includes("in use");
|
|
40891
|
+
if (!inUse || i === MAX_PORT_ATTEMPTS - 1)
|
|
40892
|
+
throw e2;
|
|
40893
|
+
log2(`Port ${actualPort} in use, trying ${actualPort + 1}...`);
|
|
40894
|
+
actualPort++;
|
|
40895
|
+
}
|
|
40896
|
+
}
|
|
40897
|
+
if (!platform3) {
|
|
40898
|
+
err(`Failed to bind a free port starting from ${port}`);
|
|
40899
|
+
process.exit(1);
|
|
40900
|
+
}
|
|
40901
|
+
ok(`Server on http://localhost:${actualPort}`);
|
|
40801
40902
|
if (platform3.contextHandler) {
|
|
40802
40903
|
ok("Commands, queries, WebSocket \u2014 all on same port");
|
|
40803
40904
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"build": "bun build --target=bun ./src/index.ts --outdir=dist --external @arcote.tech/arc --external @arcote.tech/arc-ds --external @arcote.tech/arc-react --external @arcote.tech/platform --external '@opentelemetry/*' && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@arcote.tech/arc": "^0.7.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.7.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.7.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.7.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.
|
|
20
|
-
"@arcote.tech/arc-adapter-db-postgres": "^0.7.
|
|
21
|
-
"@arcote.tech/arc-otel": "^0.7.
|
|
15
|
+
"@arcote.tech/arc": "^0.7.9",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.7.9",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.7.9",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.7.9",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.9",
|
|
20
|
+
"@arcote.tech/arc-adapter-db-postgres": "^0.7.9",
|
|
21
|
+
"@arcote.tech/arc-otel": "^0.7.9",
|
|
22
22
|
"@opentelemetry/api": "^1.9.0",
|
|
23
23
|
"@opentelemetry/api-logs": "^0.57.0",
|
|
24
24
|
"@opentelemetry/core": "^1.30.0",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
32
32
|
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.27.0",
|
|
34
|
-
"@arcote.tech/platform": "^0.7.
|
|
34
|
+
"@arcote.tech/platform": "^0.7.9",
|
|
35
35
|
"@clack/prompts": "^0.9.0",
|
|
36
36
|
"commander": "^11.1.0",
|
|
37
37
|
"chokidar": "^3.5.3",
|
|
@@ -79,11 +79,27 @@ export function collectFrameworkDeps(
|
|
|
79
79
|
`[arc-otel] could not resolve @arcote.tech/arc-otel — image will run without telemetry deps: ${(e as Error).message}`,
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
|
+
// Pole `arc` z root package.json (translations, theme, manifest...) musi
|
|
83
|
+
// trafić do obrazu runtime — `readTranslationsConfig` w server.ts czyta je
|
|
84
|
+
// z `ws.rootDir/package.json` (czyli /app/package.json w obrazie). Bez
|
|
85
|
+
// przepisania `/api/translations` zwraca pustą listę locale, `useI18nConfig`
|
|
86
|
+
// w PlatformApp nie ustawia stanu, `I18nProvider` nie zostaje mountowany
|
|
87
|
+
// i każde `useI18n()` wyrzuca błąd na produkcji.
|
|
88
|
+
let rootArc: unknown;
|
|
89
|
+
const rootPkgPath = join(rootDir, "package.json");
|
|
90
|
+
if (existsSync(rootPkgPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8")) as Record<string, unknown>;
|
|
93
|
+
if (rootPkg.arc && typeof rootPkg.arc === "object") rootArc = rootPkg.arc;
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
const manifest = {
|
|
83
98
|
name: "arc-platform-framework",
|
|
84
99
|
private: true,
|
|
85
100
|
type: "module" as const,
|
|
86
101
|
dependencies: versions,
|
|
102
|
+
...(rootArc ? { arc: rootArc } : {}),
|
|
87
103
|
};
|
|
88
104
|
const manifestPath = join(arcDir, "package.json");
|
|
89
105
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
@@ -722,8 +722,11 @@ const TAILWIND_INPUT_TEMPLATE = (rootRel: string) => `@import "tailwindcss";
|
|
|
722
722
|
|
|
723
723
|
@source "${rootRel}/packages/*/*.{ts,tsx}";
|
|
724
724
|
@source "${rootRel}/packages/*/src/**/*.{ts,tsx}";
|
|
725
|
-
|
|
726
|
-
|
|
725
|
+
/* Skanuj wszystkie framework packages — fragmenty (arc-chat, arc-ai-voice…)
|
|
726
|
+
wnoszą własne komponenty React z klasami Tailwind, które muszą trafić do
|
|
727
|
+
CSS. Bez tego absolute positioning, color/border klasy w nowych fragmentach
|
|
728
|
+
są ignorowane — komponent renderuje się bez stylów. */
|
|
729
|
+
@source "${rootRel}/node_modules/@arcote.tech/*/src/**/*.{ts,tsx}";
|
|
727
730
|
|
|
728
731
|
@custom-variant dark (&:is(.dark *));
|
|
729
732
|
|
package/src/deploy/compose.ts
CHANGED
|
@@ -164,8 +164,15 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
164
164
|
lines.push(" retries: 20");
|
|
165
165
|
lines.push(" networks:");
|
|
166
166
|
lines.push(" - arc-net");
|
|
167
|
-
//
|
|
168
|
-
// on `arc-net`.
|
|
167
|
+
// `ports:` is omitted by default — the database is only reachable from
|
|
168
|
+
// other containers on `arc-net`. When `exposeOnLoopback` is set in
|
|
169
|
+
// deploy.arc.json, bind to `127.0.0.1:<n>:5432` on the host so SSH
|
|
170
|
+
// tunneling (DBeaver/psql via `ssh -L`) works without exposing the
|
|
171
|
+
// port to the public internet (ufw only opens 22/80/443).
|
|
172
|
+
if (env.db.exposeOnLoopback !== undefined) {
|
|
173
|
+
lines.push(" ports:");
|
|
174
|
+
lines.push(` - "127.0.0.1:${env.db.exposeOnLoopback}:5432"`);
|
|
175
|
+
}
|
|
169
176
|
lines.push("");
|
|
170
177
|
}
|
|
171
178
|
|
package/src/deploy/config.ts
CHANGED
|
@@ -31,7 +31,21 @@ export interface DeployTarget {
|
|
|
31
31
|
*/
|
|
32
32
|
export type DeployEnvDb =
|
|
33
33
|
| { type: "sqlite" }
|
|
34
|
-
| {
|
|
34
|
+
| {
|
|
35
|
+
type: "postgres";
|
|
36
|
+
image?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Bind the postgres port to `127.0.0.1:<n>` on the host. Container
|
|
39
|
+
* pozostaje niewystawiony do internetu (ufw przepuszcza tylko
|
|
40
|
+
* 22/80/443), ale staje się dostępny przez SSH tunnel
|
|
41
|
+
* (`ssh -L <n>:localhost:<n> deploy@host`) np. dla DBeavera.
|
|
42
|
+
*
|
|
43
|
+
* Per-env różne porty unikają kolizji gdy kilka środowisk dzieli
|
|
44
|
+
* jeden VPS. Pomiń pole żeby zachować pełną izolację (tylko docker
|
|
45
|
+
* network).
|
|
46
|
+
*/
|
|
47
|
+
exposeOnLoopback?: number;
|
|
48
|
+
};
|
|
35
49
|
|
|
36
50
|
export interface DeployEnv {
|
|
37
51
|
/** Subdomain or full domain routed by Caddy. */
|
|
@@ -301,6 +315,10 @@ export function validateDeployConfig(input: unknown): DeployConfig {
|
|
|
301
315
|
db = {
|
|
302
316
|
type: "postgres",
|
|
303
317
|
image: optionalString(dbRaw, `envs.${name}.db.image`),
|
|
318
|
+
exposeOnLoopback: optionalNumber(
|
|
319
|
+
dbRaw,
|
|
320
|
+
`envs.${name}.db.exposeOnLoopback`,
|
|
321
|
+
),
|
|
304
322
|
};
|
|
305
323
|
} else {
|
|
306
324
|
throw new Error(
|
|
@@ -13,9 +13,11 @@ import type { DeployConfig, DeployObservability } from "./config";
|
|
|
13
13
|
// - logs: 7d retention (Loki chunks on local disk)
|
|
14
14
|
// - metrics: 30d retention (Prometheus TSDB on local disk)
|
|
15
15
|
//
|
|
16
|
-
// Tail sampling: every error + every span >500ms +
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// Tail sampling: every error + every span >500ms + 100% random przy obecnej
|
|
17
|
+
// skali. Tail_sampling matchuje OR po policies — bez "always" policy WSZYSTKIE
|
|
18
|
+
// OK traces byłyby droppowane (errors + slow nie obejmują happy path).
|
|
19
|
+
// Obniż `random_100pct` w `tail_sampling.policies` gdy backend zacznie się
|
|
20
|
+
// zatkać; do tego momentu lepiej widzieć wszystko.
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
|
|
21
23
|
const DEFAULT_RETENTION = {
|
|
@@ -60,7 +62,10 @@ processors:
|
|
|
60
62
|
send_batch_max_size: 1024
|
|
61
63
|
|
|
62
64
|
# Tail-based sampling — applied after a full trace has been assembled.
|
|
63
|
-
# Errors
|
|
65
|
+
# Errors + slow traces zachowywane w 100%, normalne traces również 100%
|
|
66
|
+
# przy obecnej skali (boostrap produkcji). Tail sampling matchuje OR po
|
|
67
|
+
# policies — bez "always" policy WSZYSTKIE OK traces byłyby droppowane.
|
|
68
|
+
# Obniż 'random_100pct' do np. 10% gdy ruch eksploduje.
|
|
64
69
|
tail_sampling:
|
|
65
70
|
decision_wait: 10s
|
|
66
71
|
num_traces: 50000
|
|
@@ -72,9 +77,9 @@ processors:
|
|
|
72
77
|
- name: slow
|
|
73
78
|
type: latency
|
|
74
79
|
latency: { threshold_ms: 500 }
|
|
75
|
-
- name:
|
|
80
|
+
- name: random_100pct
|
|
76
81
|
type: probabilistic
|
|
77
|
-
probabilistic: { sampling_percentage:
|
|
82
|
+
probabilistic: { sampling_percentage: 100 }
|
|
78
83
|
|
|
79
84
|
# Drop high-cardinality / PII attributes that might slip past app-side
|
|
80
85
|
# sanitization. Belt-and-suspenders before they hit long-term storage.
|
package/src/platform/server.ts
CHANGED
|
@@ -415,6 +415,7 @@ function apiEndpointsHandler(
|
|
|
415
415
|
getManifest: () => BuildManifest,
|
|
416
416
|
cm: ConnectionManager | null,
|
|
417
417
|
moduleAccessMap: Map<string, ModuleAccess>,
|
|
418
|
+
telemetry?: import("@arcote.tech/arc-otel").ArcTelemetry,
|
|
418
419
|
): ArcHttpHandler {
|
|
419
420
|
return (req, url, ctx) => {
|
|
420
421
|
if (url.pathname === "/api/modules") {
|
|
@@ -442,6 +443,16 @@ function apiEndpointsHandler(
|
|
|
442
443
|
status: "ok",
|
|
443
444
|
groups: Object.keys(getManifest().groups).length,
|
|
444
445
|
clients: cm?.clientCount ?? 0,
|
|
446
|
+
otel: {
|
|
447
|
+
// `enabled` — `ARC_OTEL_ENABLED` w env i init przeszedł
|
|
448
|
+
// (telemetry obiekt istnieje, config.enabled === true).
|
|
449
|
+
// `active` — to samo + `attach()` wywołane (tracer ustawiony).
|
|
450
|
+
// Rozjazd między tymi flagami zwykle oznacza że
|
|
451
|
+
// `initServerTelemetry` przeszło na wczesny return z
|
|
452
|
+
// `config.enabled === false`.
|
|
453
|
+
enabled: telemetry?.config.enabled ?? false,
|
|
454
|
+
active: telemetry?.active ?? false,
|
|
455
|
+
},
|
|
445
456
|
},
|
|
446
457
|
{ headers: ctx.corsHeaders },
|
|
447
458
|
);
|
|
@@ -510,6 +521,7 @@ export async function startPlatformServer(
|
|
|
510
521
|
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
511
522
|
mode: devMode ? "development" : "production",
|
|
512
523
|
sampleRate: devMode ? 1.0 : 1.0, // head-based 100%, collector tail-samples
|
|
524
|
+
debug: process.env.ARC_OTEL_DEBUG === "true",
|
|
513
525
|
});
|
|
514
526
|
telemetry = init.telemetry;
|
|
515
527
|
telemetryShutdown = init.shutdown;
|
|
@@ -572,7 +584,7 @@ export async function startPlatformServer(
|
|
|
572
584
|
|
|
573
585
|
// Platform handlers only
|
|
574
586
|
const handlers: ArcHttpHandler[] = [
|
|
575
|
-
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
587
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry),
|
|
576
588
|
devReloadHandler(sseClients),
|
|
577
589
|
staticFilesHandler(ws, !!devMode, getManifest),
|
|
578
590
|
spaFallbackHandler(getShellHtml),
|
|
@@ -626,7 +638,7 @@ export async function startPlatformServer(
|
|
|
626
638
|
port,
|
|
627
639
|
httpHandlers: [
|
|
628
640
|
// Platform-specific handlers (checked AFTER arc handlers)
|
|
629
|
-
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
641
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry),
|
|
630
642
|
devReloadHandler(sseClients),
|
|
631
643
|
staticFilesHandler(ws, !!devMode, getManifest),
|
|
632
644
|
spaFallbackHandler(getShellHtml),
|
package/src/platform/startup.ts
CHANGED
|
@@ -74,17 +74,39 @@ export async function startPlatform(
|
|
|
74
74
|
|
|
75
75
|
// 3. Start the platform server. Cache headers + SSE behaviour are
|
|
76
76
|
// controlled inside the server by `devMode`; we just pass the flag.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
// In dev mode, if the port is busy, try incrementing up to 20 times.
|
|
78
|
+
const MAX_PORT_ATTEMPTS = devMode ? 20 : 1;
|
|
79
|
+
let platform: PlatformServer | null = null;
|
|
80
|
+
let actualPort = port;
|
|
81
|
+
for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
|
|
82
|
+
try {
|
|
83
|
+
platform = await startPlatformServer({
|
|
84
|
+
ws,
|
|
85
|
+
port: actualPort,
|
|
86
|
+
manifest,
|
|
87
|
+
context,
|
|
88
|
+
moduleAccess,
|
|
89
|
+
dbPath,
|
|
90
|
+
devMode,
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
const msg = (e as Error).message || "";
|
|
95
|
+
const inUse =
|
|
96
|
+
msg.includes("EADDRINUSE") ||
|
|
97
|
+
msg.includes("address already in use") ||
|
|
98
|
+
msg.includes("in use");
|
|
99
|
+
if (!inUse || i === MAX_PORT_ATTEMPTS - 1) throw e;
|
|
100
|
+
log(`Port ${actualPort} in use, trying ${actualPort + 1}...`);
|
|
101
|
+
actualPort++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!platform) {
|
|
105
|
+
err(`Failed to bind a free port starting from ${port}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
86
108
|
|
|
87
|
-
ok(`Server on http://localhost:${
|
|
109
|
+
ok(`Server on http://localhost:${actualPort}`);
|
|
88
110
|
if (platform.contextHandler) {
|
|
89
111
|
ok("Commands, queries, WebSocket — all on same port");
|
|
90
112
|
}
|