@arcote.tech/arc-cli 0.7.31 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.arc/platform/_input.css +61 -0
- package/.arc/platform/access.json +1 -0
- package/.arc/platform/browser/_entries/initial.ts +2 -0
- package/.arc/platform/server/_externals.json +1 -0
- package/.arc/platform/server/_server.js +9 -0
- package/.arc/platform/server/_server.js.map +9 -0
- package/dist/index.js +915 -370
- package/package.json +10 -10
- package/src/builder/access-extractor.ts +34 -7
- package/src/builder/framework-peers.ts +7 -1
- package/src/builder/module-builder.ts +49 -11
- package/src/builder/peer-shell.ts +146 -0
- package/src/commands/platform-deploy.ts +34 -1
- package/src/commands/platform-dev.ts +7 -2
- package/src/deploy/assets/terraform/main.tf +35 -1
- package/src/deploy/assets/terraform/variables.tf +6 -0
- package/src/deploy/assets.ts +41 -1
- package/src/deploy/bootstrap.ts +9 -1
- package/src/deploy/config.ts +61 -1
- package/src/deploy/survey.ts +15 -1
- package/src/deploy/terraform.ts +3 -0
- package/src/index.ts +10 -2
- package/src/platform/server.ts +438 -45
- package/src/platform/shared.ts +119 -8
- package/src/platform/startup.ts +208 -4
package/src/deploy/config.ts
CHANGED
|
@@ -70,6 +70,22 @@ export interface DeployProvisionTerraform {
|
|
|
70
70
|
tokenEnv: string;
|
|
71
71
|
/** Path to public SSH key uploaded to the provider. Default: ~/.ssh/id_ed25519.pub */
|
|
72
72
|
sshPublicKey?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Server name in Hetzner Console + prefix for the managed SSH key.
|
|
75
|
+
* Must be unique per Hetzner project. Fallback when absent: `arc-<first env>`
|
|
76
|
+
* (legacy — kept so existing single-app deploys don't rename their server).
|
|
77
|
+
* Set this per app to run several apps in one Hetzner project without name
|
|
78
|
+
* collisions (e.g. "arc-bmf-prod").
|
|
79
|
+
*/
|
|
80
|
+
serverName?: string;
|
|
81
|
+
/**
|
|
82
|
+
* How the operator public key reaches the fresh VM.
|
|
83
|
+
* - "resource" (default, legacy): creates an `hcloud_ssh_key` object.
|
|
84
|
+
* - "cloud-init": injects the key via `user_data`, creating NO key object.
|
|
85
|
+
* Pick this when multiple apps share one Hetzner project with the same
|
|
86
|
+
* local public key — a key object would collide on name AND fingerprint.
|
|
87
|
+
*/
|
|
88
|
+
sshKeyStrategy?: "resource" | "cloud-init";
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
export interface DeployProvisionAnsible {
|
|
@@ -261,9 +277,29 @@ export function validateDeployConfig(input: unknown): DeployConfig {
|
|
|
261
277
|
);
|
|
262
278
|
}
|
|
263
279
|
|
|
280
|
+
// Host is optional when a terraform provision block is present — terraform
|
|
281
|
+
// fills it on first deploy (persisted back via saveDeployConfig). This lets
|
|
282
|
+
// a user hand-write a partial config and leave `target.host` empty. Without
|
|
283
|
+
// provision, a host is mandatory (there's nothing to create it).
|
|
284
|
+
const provisionRaw = (input as Record<string, unknown>).provision;
|
|
285
|
+
const hasProvisionTf =
|
|
286
|
+
isObject(provisionRaw) &&
|
|
287
|
+
isObject((provisionRaw as Record<string, unknown>).terraform);
|
|
288
|
+
const hostRaw = optionalString(target, "target.host");
|
|
289
|
+
let host: string;
|
|
290
|
+
if (hostRaw && hostRaw.length > 0) {
|
|
291
|
+
host = hostRaw;
|
|
292
|
+
} else if (hasProvisionTf) {
|
|
293
|
+
host = "PENDING_TERRAFORM";
|
|
294
|
+
} else {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`deploy.arc.json: target.host is required unless provision.terraform is present (with provision, terraform fills it on first deploy).`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
264
300
|
const validated: DeployConfig = {
|
|
265
301
|
target: {
|
|
266
|
-
host
|
|
302
|
+
host,
|
|
267
303
|
user: optionalString(target, "target.user") ?? "deploy",
|
|
268
304
|
port: optionalNumber(target, "target.port") ?? 22,
|
|
269
305
|
remoteDir: optionalString(target, "target.remoteDir") ?? "/opt/arc",
|
|
@@ -379,6 +415,25 @@ export function validateDeployConfig(input: unknown): DeployConfig {
|
|
|
379
415
|
`deploy.arc.json: provision.terraform.provider must be "hcloud" (got "${providerVal}")`,
|
|
380
416
|
);
|
|
381
417
|
}
|
|
418
|
+
const serverName = optionalString(tf, "provision.terraform.serverName");
|
|
419
|
+
if (serverName !== undefined && !/^[a-z0-9-]{1,63}$/.test(serverName)) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`deploy.arc.json: provision.terraform.serverName "${serverName}" must match [a-z0-9-] and be at most 63 chars`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const sshKeyStrategyRaw = optionalString(
|
|
425
|
+
tf,
|
|
426
|
+
"provision.terraform.sshKeyStrategy",
|
|
427
|
+
);
|
|
428
|
+
if (
|
|
429
|
+
sshKeyStrategyRaw !== undefined &&
|
|
430
|
+
sshKeyStrategyRaw !== "resource" &&
|
|
431
|
+
sshKeyStrategyRaw !== "cloud-init"
|
|
432
|
+
) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`deploy.arc.json: provision.terraform.sshKeyStrategy must be "resource" or "cloud-init" (got "${sshKeyStrategyRaw}")`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
382
437
|
const terraform: DeployProvisionTerraform = {
|
|
383
438
|
provider: "hcloud",
|
|
384
439
|
serverType: requireString(tf, "provision.terraform.serverType"),
|
|
@@ -386,6 +441,11 @@ export function validateDeployConfig(input: unknown): DeployConfig {
|
|
|
386
441
|
image: optionalString(tf, "provision.terraform.image") ?? "ubuntu-22.04",
|
|
387
442
|
tokenEnv: requireString(tf, "provision.terraform.tokenEnv"),
|
|
388
443
|
sshPublicKey: optionalString(tf, "provision.terraform.sshPublicKey"),
|
|
444
|
+
serverName,
|
|
445
|
+
sshKeyStrategy: sshKeyStrategyRaw as
|
|
446
|
+
| "resource"
|
|
447
|
+
| "cloud-init"
|
|
448
|
+
| undefined,
|
|
389
449
|
};
|
|
390
450
|
let ansible: DeployProvisionAnsible | undefined;
|
|
391
451
|
const ansibleRaw = (provision as Record<string, unknown>).ansible;
|
package/src/deploy/survey.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
DeployProvision,
|
|
6
6
|
DeployProvisionTerraform,
|
|
7
7
|
} from "./config";
|
|
8
|
+
import { sanitizeImageName } from "./image";
|
|
8
9
|
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Interactive init for deploy.arc.json. Grouped into minimal phases:
|
|
@@ -15,7 +16,7 @@ import type {
|
|
|
15
16
|
// Returns a validated DeployConfig ready to save. On Ctrl-C, exits the process.
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
|
|
18
|
-
export async function runSurvey(): Promise<DeployConfig> {
|
|
19
|
+
export async function runSurvey(appName?: string): Promise<DeployConfig> {
|
|
19
20
|
clack.intro("arc platform deploy — initial setup");
|
|
20
21
|
|
|
21
22
|
// Phase 1: mode
|
|
@@ -129,12 +130,25 @@ export async function runSurvey(): Promise<DeployConfig> {
|
|
|
129
130
|
})) as string;
|
|
130
131
|
if (clack.isCancel(location)) cancel();
|
|
131
132
|
|
|
133
|
+
// Namespace the server per app and inject the SSH key via cloud-init, so
|
|
134
|
+
// several apps can share one Hetzner project without colliding on server
|
|
135
|
+
// name or SSH key name/fingerprint. Both are opt-in fields with legacy
|
|
136
|
+
// fallbacks, but new configs default to the collision-safe combo.
|
|
137
|
+
const firstEnv = Object.keys(envs)[0] ?? "host";
|
|
138
|
+
const appSlug = sanitizeImageName(appName ?? "arc-app").replace(
|
|
139
|
+
/[^a-z0-9-]+/g,
|
|
140
|
+
"-",
|
|
141
|
+
);
|
|
142
|
+
const serverName = `arc-${appSlug}-${firstEnv}`.slice(0, 63);
|
|
143
|
+
|
|
132
144
|
const terraform: DeployProvisionTerraform = {
|
|
133
145
|
provider: "hcloud",
|
|
134
146
|
serverType,
|
|
135
147
|
location,
|
|
136
148
|
image: "ubuntu-22.04",
|
|
137
149
|
tokenEnv,
|
|
150
|
+
serverName,
|
|
151
|
+
sshKeyStrategy: "cloud-init",
|
|
138
152
|
};
|
|
139
153
|
provision = { terraform };
|
|
140
154
|
}
|
package/src/deploy/terraform.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface TerraformInputs {
|
|
|
20
20
|
token: string;
|
|
21
21
|
/** Deterministic server name shown in Hetzner Console. */
|
|
22
22
|
serverName: string;
|
|
23
|
+
/** Inject SSH key via cloud-init user_data instead of a managed key object. */
|
|
24
|
+
useCloudInitKey: boolean;
|
|
23
25
|
/** Workspace root — used to scope state dir per project. */
|
|
24
26
|
workspaceDir: string;
|
|
25
27
|
}
|
|
@@ -61,6 +63,7 @@ export async function runTerraform(
|
|
|
61
63
|
`server_location = "${inputs.tf.location}"`,
|
|
62
64
|
`server_image = "${inputs.tf.image}"`,
|
|
63
65
|
`ssh_public_key = "${expandHome(sshPubKey)}"`,
|
|
66
|
+
`use_cloud_init_key = ${inputs.useCloudInitKey}`,
|
|
64
67
|
].join("\n") + "\n";
|
|
65
68
|
|
|
66
69
|
writeFileSync(join(workDir, "terraform.tfvars"), tfvars);
|
package/src/index.ts
CHANGED
|
@@ -35,8 +35,16 @@ platform
|
|
|
35
35
|
.description("Start platform in dev mode (Bun server + Vite HMR)")
|
|
36
36
|
.option("--no-cache", "Force full rebuild on startup")
|
|
37
37
|
.option("--map", "Also start the Arc Context Map dev tool on a separate port")
|
|
38
|
-
.
|
|
39
|
-
|
|
38
|
+
.option(
|
|
39
|
+
"--headless",
|
|
40
|
+
"Host API + federated module chunks + map, WITHOUT the app's own SPA shell (implies --map)",
|
|
41
|
+
)
|
|
42
|
+
.action((opts: { cache?: boolean; map?: boolean; headless?: boolean }) =>
|
|
43
|
+
platformDev({
|
|
44
|
+
noCache: opts.cache === false,
|
|
45
|
+
map: opts.map,
|
|
46
|
+
headless: opts.headless,
|
|
47
|
+
}),
|
|
40
48
|
);
|
|
41
49
|
|
|
42
50
|
platform
|