@arcote.tech/arc-cli 0.7.6 → 0.7.8
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 +12036 -28947
- package/package.json +22 -10
- package/src/builder/dependency-collector.ts +34 -1
- package/src/commands/platform-deploy.ts +6 -0
- package/src/deploy/bootstrap.ts +35 -7
- package/src/deploy/compose.ts +12 -0
- package/src/deploy/htpasswd.ts +20 -0
- package/src/deploy/observability-configs.ts +674 -9
- package/src/platform/server.ts +19 -1
- package/src/platform/startup.ts +32 -10
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.8",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,17 +9,29 @@
|
|
|
9
9
|
"arc": "./dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
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 && chmod +x dist/index.js"
|
|
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.
|
|
22
|
-
"@
|
|
15
|
+
"@arcote.tech/arc": "^0.7.8",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.7.8",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.7.8",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.7.8",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.8",
|
|
20
|
+
"@arcote.tech/arc-adapter-db-postgres": "^0.7.8",
|
|
21
|
+
"@arcote.tech/arc-otel": "^0.7.8",
|
|
22
|
+
"@opentelemetry/api": "^1.9.0",
|
|
23
|
+
"@opentelemetry/api-logs": "^0.57.0",
|
|
24
|
+
"@opentelemetry/core": "^1.30.0",
|
|
25
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.57.0",
|
|
26
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.57.0",
|
|
27
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
|
28
|
+
"@opentelemetry/resources": "^1.30.0",
|
|
29
|
+
"@opentelemetry/sdk-logs": "^0.57.0",
|
|
30
|
+
"@opentelemetry/sdk-metrics": "^1.30.0",
|
|
31
|
+
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
32
|
+
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
33
|
+
"@opentelemetry/semantic-conventions": "^1.27.0",
|
|
34
|
+
"@arcote.tech/platform": "^0.7.8",
|
|
23
35
|
"@clack/prompts": "^0.9.0",
|
|
24
36
|
"commander": "^11.1.0",
|
|
25
37
|
"chokidar": "^3.5.3",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
-
import { basename, join } from "path";
|
|
3
|
+
import { basename, dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { FRAMEWORK_PEERS, FRAMEWORK_PEER_SET } from "./framework-peers";
|
|
5
6
|
import type { WorkspacePackage } from "./module-builder";
|
|
6
7
|
|
|
@@ -46,6 +47,38 @@ export function collectFrameworkDeps(
|
|
|
46
47
|
for (const { name, version } of sharedDeps) {
|
|
47
48
|
versions[name] = version;
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
// OpenTelemetry deps — included unconditionally so deployed images can
|
|
52
|
+
// resolve them at runtime when telemetry is enabled. Cheap install
|
|
53
|
+
// (~5MB) and avoids per-deploy branching on `observability.enabled`.
|
|
54
|
+
// CLI's bundle externalizes @opentelemetry/* to dodge a Bun bundling
|
|
55
|
+
// bug that mixes ES5/ES6 class semantics across these packages.
|
|
56
|
+
// Resolve arc-otel from the CLI bundle's own location: arc-otel is a
|
|
57
|
+
// dep of arc-cli, not of the consumer app, so it isn't reachable from
|
|
58
|
+
// `rootDir`. Bun installs it nested under `.bun/<arc-cli-id>/...`,
|
|
59
|
+
// visible to a resolve call rooted in arc-cli's own dist directory.
|
|
60
|
+
try {
|
|
61
|
+
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
const arcOtelPkgPath = Bun.resolveSync(
|
|
63
|
+
"@arcote.tech/arc-otel/package.json",
|
|
64
|
+
cliDir,
|
|
65
|
+
);
|
|
66
|
+
const arcOtelPkg = JSON.parse(readFileSync(arcOtelPkgPath, "utf-8"));
|
|
67
|
+
for (const [name, spec] of Object.entries(arcOtelPkg.dependencies ?? {})) {
|
|
68
|
+
if (name.startsWith("@opentelemetry/")) {
|
|
69
|
+
versions[name] = spec as string;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
versions["@arcote.tech/arc-otel"] = `^${arcOtelPkg.version}`;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// arc-otel not installed alongside the CLI — older CLI release without
|
|
75
|
+
// observability support, or a bundle that stripped the dep. Log once
|
|
76
|
+
// so a misconfigured deploy doesn't silently disable telemetry libs
|
|
77
|
+
// when the operator was expecting them.
|
|
78
|
+
console.warn(
|
|
79
|
+
`[arc-otel] could not resolve @arcote.tech/arc-otel — image will run without telemetry deps: ${(e as Error).message}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
49
82
|
const manifest = {
|
|
50
83
|
name: "arc-platform-framework",
|
|
51
84
|
private: true,
|
|
@@ -235,5 +235,11 @@ async function hashDeployConfig(rootDir: string): Promise<string> {
|
|
|
235
235
|
const content = readFileSync(p);
|
|
236
236
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
237
237
|
hasher.update(content);
|
|
238
|
+
// Mix in the CLI version so a new CLI release that changes compose
|
|
239
|
+
// / caddyfile / observability templates forces upStack to re-run even
|
|
240
|
+
// when the user's deploy.arc.json is byte-identical. Without this,
|
|
241
|
+
// template changes ship in the npm package but never reach the host
|
|
242
|
+
// because the marker says "no config drift, skipping bootstrap".
|
|
243
|
+
hasher.update(readCliVersion());
|
|
238
244
|
return hasher.digest("hex").slice(0, 16);
|
|
239
245
|
}
|
package/src/deploy/bootstrap.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { spawn } from "bun";
|
|
2
2
|
import { mkdirSync, writeFileSync } from "fs";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
5
|
import { runAnsible } from "./ansible";
|
|
6
6
|
import { generateCaddyfile } from "./caddyfile";
|
|
7
7
|
import { generateCompose } from "./compose";
|
|
8
8
|
import type { DeployConfig } from "./config";
|
|
9
|
-
import { generateHtpasswd } from "./htpasswd";
|
|
9
|
+
import { generateCaddyBasicAuthLine, generateHtpasswd } from "./htpasswd";
|
|
10
10
|
import { generateObservabilityConfigs } from "./observability-configs";
|
|
11
11
|
import { runTerraform } from "./terraform";
|
|
12
12
|
import { saveDeployConfig } from "./config";
|
|
@@ -148,6 +148,22 @@ export async function bootstrap(inputs: BootstrapInputs): Promise<void> {
|
|
|
148
148
|
ok("Docker stack up");
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// Observability sidecars — always reconcile state, independent of
|
|
152
|
+
// upStack. `docker compose up -d` is idempotent: when the containers
|
|
153
|
+
// already match the current compose definition, nothing happens. When
|
|
154
|
+
// the user just turned observability on (config changed but upStack
|
|
155
|
+
// ran in a previous deploy), this brings the new services to life
|
|
156
|
+
// without forcing a full bootstrap.
|
|
157
|
+
if (cfg.observability?.enabled) {
|
|
158
|
+
log("Ensuring observability sidecars are running...");
|
|
159
|
+
const obsServices = ["otel-collector", "tempo", "loki", "prometheus", "grafana"];
|
|
160
|
+
await assertExec(
|
|
161
|
+
cfg.target,
|
|
162
|
+
`cd ${cfg.target.remoteDir} && docker compose pull --ignore-pull-failures ${obsServices.join(" ")} && docker compose up -d ${obsServices.join(" ")}`,
|
|
163
|
+
);
|
|
164
|
+
ok("Observability stack up");
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
// Keep marker fresh
|
|
152
168
|
await writeStateMarker(cfg.target, {
|
|
153
169
|
cliVersion: inputs.cliVersion,
|
|
@@ -241,11 +257,13 @@ async function upStack(inputs: BootstrapInputs): Promise<void> {
|
|
|
241
257
|
`bootstrap: ${adminPasswordEnv} not set — observability needs a Grafana admin password.`,
|
|
242
258
|
);
|
|
243
259
|
}
|
|
244
|
-
// Caddy basic_auth import file uses `username bcrypt_hash` syntax
|
|
245
|
-
|
|
246
|
-
|
|
260
|
+
// Caddy basic_auth import file uses `username bcrypt_hash` syntax
|
|
261
|
+
// (space-separated), NOT the registry-style htpasswd colon format.
|
|
262
|
+
observabilityHtpasswd = await generateCaddyBasicAuthLine("admin", adminPassword);
|
|
247
263
|
for (const [relPath, contents] of Object.entries(observabilityFiles)) {
|
|
248
|
-
|
|
264
|
+
const fullPath = join(workDir, relPath);
|
|
265
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
266
|
+
writeFileSync(fullPath, contents);
|
|
249
267
|
}
|
|
250
268
|
writeFileSync(join(workDir, "observability-htpasswd"), observabilityHtpasswd);
|
|
251
269
|
}
|
|
@@ -288,10 +306,19 @@ async function upStack(inputs: BootstrapInputs): Promise<void> {
|
|
|
288
306
|
// tempo / loki / prometheus / grafana configs go into a sibling
|
|
289
307
|
// `observability/` directory referenced by compose bind-mounts.
|
|
290
308
|
if (observabilityFiles && observabilityHtpasswd) {
|
|
309
|
+
// Make sure both the top-level `observability/` dir and the nested
|
|
310
|
+
// `grafana-dashboards/` exist before scp tries to land files there.
|
|
291
311
|
await assertExec(
|
|
292
312
|
cfg.target,
|
|
293
|
-
`mkdir -p ${cfg.target.remoteDir}/observability`,
|
|
313
|
+
`mkdir -p ${cfg.target.remoteDir}/observability/grafana-dashboards`,
|
|
294
314
|
);
|
|
315
|
+
// Make sure local nested dir exists too — `generateObservabilityConfigs`
|
|
316
|
+
// returns relative paths like `observability/grafana-dashboards/x.json`
|
|
317
|
+
// which mkdirSync below uses for writeFileSync's directory.
|
|
318
|
+
for (const relPath of Object.keys(observabilityFiles)) {
|
|
319
|
+
const localDir = dirname(join(workDir, relPath));
|
|
320
|
+
mkdirSync(localDir, { recursive: true });
|
|
321
|
+
}
|
|
295
322
|
for (const relPath of Object.keys(observabilityFiles)) {
|
|
296
323
|
await scpUpload(
|
|
297
324
|
cfg.target,
|
|
@@ -371,6 +398,7 @@ async function upStack(inputs: BootstrapInputs): Promise<void> {
|
|
|
371
398
|
.join(" ")}`,
|
|
372
399
|
);
|
|
373
400
|
}
|
|
401
|
+
|
|
374
402
|
}
|
|
375
403
|
|
|
376
404
|
/**
|
package/src/deploy/compose.ts
CHANGED
|
@@ -224,6 +224,7 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
224
224
|
lines.push(" - \"3100\" # HTTP API");
|
|
225
225
|
lines.push("");
|
|
226
226
|
|
|
227
|
+
const metricsRetention = cfg.observability.retention?.metrics ?? "30d";
|
|
227
228
|
lines.push(" prometheus:");
|
|
228
229
|
lines.push(" image: prom/prometheus:v2.55.1");
|
|
229
230
|
lines.push(" container_name: arc-prometheus");
|
|
@@ -231,6 +232,7 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
231
232
|
lines.push(" command:");
|
|
232
233
|
lines.push(" - \"--config.file=/etc/prometheus/prometheus.yml\"");
|
|
233
234
|
lines.push(" - \"--storage.tsdb.path=/prometheus\"");
|
|
235
|
+
lines.push(` - "--storage.tsdb.retention.time=${metricsRetention}"`);
|
|
234
236
|
lines.push(" - \"--web.enable-remote-write-receiver\"");
|
|
235
237
|
lines.push(" - \"--enable-feature=exemplar-storage\"");
|
|
236
238
|
lines.push(" volumes:");
|
|
@@ -259,6 +261,16 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
259
261
|
lines.push(
|
|
260
262
|
" - ./observability/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml:ro",
|
|
261
263
|
);
|
|
264
|
+
// Dashboard auto-provisioning: a single provider config points at the
|
|
265
|
+
// bind-mounted /arc directory; each JSON in that dir becomes a live
|
|
266
|
+
// dashboard inside the "Arc" folder. Updates land within the
|
|
267
|
+
// provider's `updateIntervalSeconds` (30s).
|
|
268
|
+
lines.push(
|
|
269
|
+
" - ./observability/grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml:ro",
|
|
270
|
+
);
|
|
271
|
+
lines.push(
|
|
272
|
+
" - ./observability/grafana-dashboards:/etc/grafana/provisioning/dashboards/arc:ro",
|
|
273
|
+
);
|
|
262
274
|
lines.push(" - grafana_data:/var/lib/grafana");
|
|
263
275
|
lines.push(" networks: [arc-net]");
|
|
264
276
|
lines.push(" expose:");
|
package/src/deploy/htpasswd.ts
CHANGED
|
@@ -26,3 +26,23 @@ export async function generateHtpasswd(
|
|
|
26
26
|
});
|
|
27
27
|
return `${user}:${hash}\n`;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Caddy's `basic_auth` directive takes a different format than the
|
|
32
|
+
* registry-style htpasswd file: `<user> <bcrypt-hash>` (space-separated,
|
|
33
|
+
* no colon). Newer Caddy syntax also accepts a quoted hash, but the
|
|
34
|
+
* unquoted bcrypt string works in both 2.7 and 2.8+.
|
|
35
|
+
*/
|
|
36
|
+
export async function generateCaddyBasicAuthLine(
|
|
37
|
+
user: string,
|
|
38
|
+
password: string,
|
|
39
|
+
): Promise<string> {
|
|
40
|
+
if (!user || !password) {
|
|
41
|
+
throw new Error("caddy basic_auth: user and password must both be non-empty");
|
|
42
|
+
}
|
|
43
|
+
const hash = await Bun.password.hash(password, {
|
|
44
|
+
algorithm: "bcrypt",
|
|
45
|
+
cost: 10,
|
|
46
|
+
});
|
|
47
|
+
return `${user} ${hash}\n`;
|
|
48
|
+
}
|