@clawscarf/cli 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/THIRD_PARTY_NOTICES.md +102 -0
- package/deploy/execution/browser/LICENSE.playwright +202 -0
- package/deploy/execution/browser/seccomp.json +640 -0
- package/deploy/execution/browser-node/configuration.js +85 -0
- package/deploy/execution/browser-node/operator.js +167 -0
- package/deploy/execution/network/node-ingress.cfg +35 -0
- package/deploy/models/catalog.json +275 -0
- package/deploy/openshell/policy.yaml +24 -0
- package/generated/http/LICENSE.md +21 -0
- package/generated/http/client/client.gen.d.ts +2 -0
- package/generated/http/client/client.gen.js +216 -0
- package/generated/http/client/index.d.ts +10 -0
- package/generated/http/client/index.js +6 -0
- package/generated/http/client/types.gen.d.ts +120 -0
- package/generated/http/client/types.gen.js +2 -0
- package/generated/http/client/utils.gen.d.ts +37 -0
- package/generated/http/client/utils.gen.js +228 -0
- package/generated/http/core/auth.gen.d.ts +25 -0
- package/generated/http/core/auth.gen.js +14 -0
- package/generated/http/core/bodySerializer.gen.d.ts +25 -0
- package/generated/http/core/bodySerializer.gen.js +57 -0
- package/generated/http/core/params.gen.d.ts +43 -0
- package/generated/http/core/params.gen.js +109 -0
- package/generated/http/core/pathSerializer.gen.d.ts +33 -0
- package/generated/http/core/pathSerializer.gen.js +106 -0
- package/generated/http/core/queryKeySerializer.gen.d.ts +18 -0
- package/generated/http/core/queryKeySerializer.gen.js +92 -0
- package/generated/http/core/serverSentEvents.gen.d.ts +71 -0
- package/generated/http/core/serverSentEvents.gen.js +132 -0
- package/generated/http/core/types.gen.d.ts +83 -0
- package/generated/http/core/types.gen.js +2 -0
- package/generated/http/core/utils.gen.d.ts +19 -0
- package/generated/http/core/utils.gen.js +87 -0
- package/package.json +37 -0
- package/packs/README.md +130 -0
- package/packs/research-team/pack.json +20 -0
- package/packs/research-team/researcher/CLAW.md +19 -0
- package/packs/research-team/researcher/package.json +8 -0
- package/packs/research-team/researcher/profiles/openclaw.yml +6 -0
- package/packs/research-team/reviewer/CLAW.md +19 -0
- package/packs/research-team/reviewer/package.json +8 -0
- package/packs/research-team/reviewer/profiles/openclaw.yml +6 -0
- package/pnpm-lock.yaml +7158 -0
- package/recipes/README.md +37 -0
- package/recipes/team-server/recipe.json +28 -0
- package/release/README.md +137 -0
- package/release/components.json +29 -0
- package/release/operator.md +63 -0
- package/runtime/configuration.js +135 -0
- package/runtime/connections-configuration.js +70 -0
- package/runtime/model-contract.js +92 -0
- package/runtime/releases/0.1.0-alpha.1.json +37 -0
- package/scripts/clawscarf.js +92 -0
- package/scripts/cloud/login.js +134 -0
- package/scripts/cloud/registration.js +232 -0
- package/scripts/cloud/url.js +9 -0
- package/scripts/connections.js +193 -0
- package/scripts/controller.js +143 -0
- package/scripts/deployment/browser-node-compose.js +81 -0
- package/scripts/deployment/browser-node-helper.js +9 -0
- package/scripts/deployment/browser-node-pairing.js +165 -0
- package/scripts/deployment/browser-node.js +154 -0
- package/scripts/deployment/browser.js +143 -0
- package/scripts/deployment/certificates.js +76 -0
- package/scripts/deployment/compose.js +191 -0
- package/scripts/deployment/configuration.js +275 -0
- package/scripts/deployment/connection-policy.js +80 -0
- package/scripts/deployment/connection-settings.js +60 -0
- package/scripts/deployment/connections-runtime.js +31 -0
- package/scripts/deployment/connections.js +213 -0
- package/scripts/deployment/controller-compose.js +59 -0
- package/scripts/deployment/database.js +199 -0
- package/scripts/deployment/entrypoint.js +7 -0
- package/scripts/deployment/images.js +17 -0
- package/scripts/deployment/initial-services.js +53 -0
- package/scripts/deployment/launch.js +177 -0
- package/scripts/deployment/logs.js +15 -0
- package/scripts/deployment/model-gateway-compose.js +74 -0
- package/scripts/deployment/model-gateway.js +125 -0
- package/scripts/deployment/models.js +82 -0
- package/scripts/deployment/networks.js +354 -0
- package/scripts/deployment/policy.js +56 -0
- package/scripts/deployment/preflight.js +124 -0
- package/scripts/deployment/prepare.js +205 -0
- package/scripts/deployment/process.js +70 -0
- package/scripts/deployment/relay.js +58 -0
- package/scripts/deployment/runtime-binding.js +100 -0
- package/scripts/deployment/runtime.js +232 -0
- package/scripts/deployment/state.js +154 -0
- package/scripts/deployment/team.js +76 -0
- package/scripts/deployment/upgrade-rpc.py +148 -0
- package/scripts/deployment/upgrade-state.js +47 -0
- package/scripts/deployment/upgrade.js +298 -0
- package/scripts/deployment/volumes.js +24 -0
- package/scripts/errors.js +9 -0
- package/scripts/installation/administrator.js +23 -0
- package/scripts/installation/command.js +176 -0
- package/scripts/installation/configuration.js +104 -0
- package/scripts/installation/configure.js +122 -0
- package/scripts/installation/delete.js +111 -0
- package/scripts/installation/doctor.js +8 -0
- package/scripts/installation/errors.js +8 -0
- package/scripts/installation/files.js +51 -0
- package/scripts/installation/installer/cloud.js +22 -0
- package/scripts/installation/installer/collect.js +245 -0
- package/scripts/installation/installer/inputs.js +59 -0
- package/scripts/installation/installer/menu.js +49 -0
- package/scripts/installation/installer/prompts.js +139 -0
- package/scripts/installation/installer/run.js +251 -0
- package/scripts/installation/installer/secrets.js +16 -0
- package/scripts/installation/installer/sections/access.js +69 -0
- package/scripts/installation/installer/sections/certificates.js +18 -0
- package/scripts/installation/installer/sections/connections.js +4 -0
- package/scripts/installation/installer/sections/external-models.js +35 -0
- package/scripts/installation/installer/sections/models.js +119 -0
- package/scripts/installation/installer/sections/packs.js +82 -0
- package/scripts/installation/installer/sections/resources.js +10 -0
- package/scripts/installation/installer/settings.js +192 -0
- package/scripts/installation/installer/summary.js +44 -0
- package/scripts/installation/lifecycle.js +127 -0
- package/scripts/installation/location.js +3 -0
- package/scripts/installation/models.js +42 -0
- package/scripts/installation/options.js +360 -0
- package/scripts/installation/packs.js +150 -0
- package/scripts/installation/plan.js +121 -0
- package/scripts/installation/prerequisites.js +170 -0
- package/scripts/installation/recipes/catalog.js +55 -0
- package/scripts/installation/recipes/definition.js +32 -0
- package/scripts/installation/reconfigure.js +255 -0
- package/scripts/installation/requirements.js +23 -0
- package/scripts/installation/resolve.js +217 -0
- package/scripts/installation/runtime.js +87 -0
- package/scripts/installation/save.js +95 -0
- package/scripts/installation/setup.js +91 -0
- package/scripts/models/catalog.js +9 -0
- package/scripts/models/configuration.js +184 -0
- package/scripts/models/credentials.js +132 -0
- package/scripts/models/runtime.js +87 -0
- package/scripts/output.js +34 -0
- package/scripts/packs/connections.js +95 -0
- package/scripts/packs/lifecycle.js +125 -0
- package/scripts/packs/model.js +66 -0
- package/scripts/packs/native.js +69 -0
- package/scripts/packs/openshell.js +148 -0
- package/scripts/packs/policy.js +189 -0
- package/scripts/packs/requirements.in +1 -0
- package/scripts/packs/requirements.js +11 -0
- package/scripts/packs/requirements.txt +116 -0
- package/scripts/packs/source.js +90 -0
- package/scripts/packs/transport.py +68 -0
- package/scripts/people.js +52 -0
- package/scripts/release/create.js +62 -0
- package/scripts/release/definition.js +51 -0
- package/scripts/session.js +27 -0
- package/services/access/generated/client.gen.d.ts +12 -0
- package/services/access/generated/client.gen.js +3 -0
- package/services/access/generated/fastify.gen.d.ts +49 -0
- package/services/access/generated/fastify.gen.js +2 -0
- package/services/access/generated/index.d.ts +2 -0
- package/services/access/generated/index.js +2 -0
- package/services/access/generated/sdk.gen.d.ts +28 -0
- package/services/access/generated/sdk.gen.js +107 -0
- package/services/access/generated/types.gen.d.ts +349 -0
- package/services/access/generated/types.gen.js +2 -0
- package/services/access/migrations/001_access.sql +39 -0
- package/services/access/migrations/002_administrator_setup.sql +16 -0
- package/services/access/migrations/003_local_login_completion.sql +5 -0
- package/services/access/migrations/004_invitations.sql +21 -0
- package/services/access/repo/postgres.js +341 -0
- package/services/access/repo/session-logout.js +28 -0
- package/services/access/types/credential.js +2 -0
- package/services/access/types/errors.js +7 -0
- package/services/access/types/ingress.js +1 -0
- package/services/access/types/model.js +1 -0
- package/services/access/types/native-errors.js +7 -0
- package/services/access/types/native.js +1 -0
- package/services/cloud/generated/client.gen.d.ts +12 -0
- package/services/cloud/generated/client.gen.js +3 -0
- package/services/cloud/generated/index.d.ts +2 -0
- package/services/cloud/generated/index.js +2 -0
- package/services/cloud/generated/sdk.gen.d.ts +145 -0
- package/services/cloud/generated/sdk.gen.js +488 -0
- package/services/cloud/generated/types.gen.d.ts +2034 -0
- package/services/cloud/generated/types.gen.js +2 -0
- package/services/connections/cloud/generated/client.gen.d.ts +12 -0
- package/services/connections/cloud/generated/client.gen.js +3 -0
- package/services/connections/cloud/generated/fastify.gen.d.ts +68 -0
- package/services/connections/cloud/generated/fastify.gen.js +2 -0
- package/services/connections/cloud/generated/index.d.ts +2 -0
- package/services/connections/cloud/generated/index.js +2 -0
- package/services/connections/cloud/generated/sdk.gen.d.ts +95 -0
- package/services/connections/cloud/generated/sdk.gen.js +210 -0
- package/services/connections/cloud/generated/types.gen.d.ts +1044 -0
- package/services/connections/cloud/generated/types.gen.js +2 -0
- package/services/connections/migrations/001_connections.sql +220 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
function row(value, label, summary, hint) {
|
|
3
|
+
return {
|
|
4
|
+
value,
|
|
5
|
+
label: `${label.padEnd(24)}${summary}`,
|
|
6
|
+
...(hint ? { hint } : {}),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function installationMenu(config, directory, browserAvailable, packIssues, modelSummary, existing = false) {
|
|
10
|
+
const location = directory.startsWith(homedir() + "/")
|
|
11
|
+
? "~" + directory.slice(homedir().length)
|
|
12
|
+
: directory;
|
|
13
|
+
const agents = config.packs.flatMap((pack) => pack.members);
|
|
14
|
+
const choices = [
|
|
15
|
+
{
|
|
16
|
+
value: "review",
|
|
17
|
+
label: existing ? "Review and apply\n" : "Accept settings and continue\n",
|
|
18
|
+
hint: existing
|
|
19
|
+
? "Review before changing the server"
|
|
20
|
+
: "Credentials come next",
|
|
21
|
+
},
|
|
22
|
+
row("location", "Location", location.length > 40 ? "…" + location.slice(-39) : location, directory),
|
|
23
|
+
row("identity", "Name and administrator", `${config.name} · ${config.access.administratorName}`),
|
|
24
|
+
row("access", "Access", config.access.mode === "hosted" ? "ClawScarf login" : "Custom OIDC", "Configure your own OIDC provider (optional)"),
|
|
25
|
+
row("exposure", "Network", config.exposure.mode === "local"
|
|
26
|
+
? "This computer"
|
|
27
|
+
: config.exposure.applicationOrigin),
|
|
28
|
+
row("models", "Models", modelSummary ?? "Choose a model"),
|
|
29
|
+
row("connections", "Connections", config.connections.mode === "disabled" ? "Off" : "On"),
|
|
30
|
+
row("packs", "Packs", packIssues.length
|
|
31
|
+
? "Needs attention"
|
|
32
|
+
: agents.length
|
|
33
|
+
? `${String(agents.length)} agents selected`
|
|
34
|
+
: "None", packIssues.length ? packIssues.join(" ") : agents.join(", ")),
|
|
35
|
+
row("resources", "Resources", `${config.resources.runtime.cpu} CPU / ${config.resources.runtime.memory}`, "Protected team runtime"),
|
|
36
|
+
...(browserAvailable
|
|
37
|
+
? [
|
|
38
|
+
row("browser", "Browser", config.browser.enabled ? "On (experimental)" : "Off"),
|
|
39
|
+
]
|
|
40
|
+
: []),
|
|
41
|
+
{ value: "advanced-models", label: "Advanced model gateway settings" },
|
|
42
|
+
];
|
|
43
|
+
return existing
|
|
44
|
+
? [
|
|
45
|
+
...choices.filter(({ value }) => ["review", "models", "connections", "packs"].includes(value)),
|
|
46
|
+
{ value: "model-credentials", label: "Change LLM API keys" },
|
|
47
|
+
]
|
|
48
|
+
: choices;
|
|
49
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import * as clack from "@clack/prompts";
|
|
4
|
+
import { InstallationError } from "../errors.js";
|
|
5
|
+
export class InstallerCancelled extends Error {
|
|
6
|
+
}
|
|
7
|
+
export class SectionCancelled extends Error {
|
|
8
|
+
}
|
|
9
|
+
/** OSC 8 adds terminal links; the visible URL also works in terminals without it. */
|
|
10
|
+
export function terminalLink(url) {
|
|
11
|
+
return process.stdout.isTTY && process.env.TERM !== "dumb"
|
|
12
|
+
? `\u001b]8;;${url}\u001b\\${url}\u001b]8;;\u001b\\`
|
|
13
|
+
: url;
|
|
14
|
+
}
|
|
15
|
+
async function answer(work) {
|
|
16
|
+
const navigation = { back: false };
|
|
17
|
+
const keypress = (_text, key) => {
|
|
18
|
+
if (key.name === "escape")
|
|
19
|
+
navigation.back = true;
|
|
20
|
+
};
|
|
21
|
+
process.stdin.on("keypress", keypress);
|
|
22
|
+
try {
|
|
23
|
+
const value = await work();
|
|
24
|
+
if (typeof value === "symbol") {
|
|
25
|
+
if (navigation.back)
|
|
26
|
+
throw new SectionCancelled();
|
|
27
|
+
throw new InstallerCancelled();
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
process.stdin.off("keypress", keypress);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const terminalPrompts = {
|
|
36
|
+
async text(message, initial, validate) {
|
|
37
|
+
return answer(() => clack.text({
|
|
38
|
+
message,
|
|
39
|
+
...(initial === undefined ? {} : { initialValue: initial }),
|
|
40
|
+
validate: (value) => validate
|
|
41
|
+
? validate(value ?? "")
|
|
42
|
+
: value?.trim()
|
|
43
|
+
? undefined
|
|
44
|
+
: "Enter a value.",
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
async password(message) {
|
|
48
|
+
return answer(() => clack.password({
|
|
49
|
+
message,
|
|
50
|
+
validate: (value) => (value?.trim() ? undefined : "Enter a value."),
|
|
51
|
+
}));
|
|
52
|
+
},
|
|
53
|
+
async select(message, options, initial) {
|
|
54
|
+
return answer(() => clack.select({
|
|
55
|
+
message,
|
|
56
|
+
options,
|
|
57
|
+
...(initial === undefined ? {} : { initialValue: initial }),
|
|
58
|
+
}));
|
|
59
|
+
},
|
|
60
|
+
async multiselect(message, options) {
|
|
61
|
+
return answer(() => clack.multiselect({ message, options, required: true }));
|
|
62
|
+
},
|
|
63
|
+
async confirm(message, initialValue = false) {
|
|
64
|
+
return answer(() => clack.confirm({ message, initialValue }));
|
|
65
|
+
},
|
|
66
|
+
note: clack.note,
|
|
67
|
+
async openBrowser(url) {
|
|
68
|
+
try {
|
|
69
|
+
await promisify(execFile)(process.platform === "darwin" ? "open" : "xdg-open", [url], { timeout: 5000 });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
process.stderr.write("Could not open a browser. Open the link above manually.\n");
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
export function requireTerminal() {
|
|
77
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
78
|
+
throw new InstallationError("invalid_configuration", "Use configure --non-interactive with explicit options when no terminal is available.");
|
|
79
|
+
}
|
|
80
|
+
export async function progress(message, work, options = {}) {
|
|
81
|
+
const cancellation = new AbortController();
|
|
82
|
+
const cancel = () => {
|
|
83
|
+
if (cancellation.signal.aborted)
|
|
84
|
+
return;
|
|
85
|
+
cancellation.abort(new InstallerCancelled());
|
|
86
|
+
process.stderr.write("Cancellation requested. Waiting for the current operation to settle; no following step will start.\n");
|
|
87
|
+
};
|
|
88
|
+
const spinner = !options.json && process.stderr.isTTY
|
|
89
|
+
? clack.spinner({ output: process.stderr, onCancel: cancel })
|
|
90
|
+
: undefined;
|
|
91
|
+
const report = (detail) => {
|
|
92
|
+
if (spinner && detail.includes("\n")) {
|
|
93
|
+
spinner.stop(message);
|
|
94
|
+
process.stderr.write(detail + "\n");
|
|
95
|
+
spinner.start(message);
|
|
96
|
+
}
|
|
97
|
+
else if (spinner)
|
|
98
|
+
spinner.message(detail);
|
|
99
|
+
else
|
|
100
|
+
process.stderr.write(detail + "\n");
|
|
101
|
+
};
|
|
102
|
+
if (spinner)
|
|
103
|
+
spinner.start(message);
|
|
104
|
+
else {
|
|
105
|
+
process.once("SIGINT", cancel);
|
|
106
|
+
report(message);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const result = await work(cancellation.signal, report);
|
|
110
|
+
cancellation.signal.throwIfAborted();
|
|
111
|
+
spinner?.stop(message);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof InstallerCancelled)
|
|
116
|
+
spinner?.stop("Stopped.");
|
|
117
|
+
else
|
|
118
|
+
spinner?.error("Could not complete this step.");
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
process.off("SIGINT", cancel);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** No unattended operation may fall through into an interactive question. */
|
|
126
|
+
export const unattendedPrompts = {
|
|
127
|
+
openBrowser: () => Promise.resolve(),
|
|
128
|
+
text: missing,
|
|
129
|
+
password: missing,
|
|
130
|
+
select: missing,
|
|
131
|
+
multiselect: missing,
|
|
132
|
+
confirm: missing,
|
|
133
|
+
note(message, title) {
|
|
134
|
+
process.stderr.write(`${title}: ${message}\n`);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
function missing(message) {
|
|
138
|
+
return Promise.reject(new InstallationError("invalid_configuration", `Missing unattended input: ${message}`));
|
|
139
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { defaultInstallationDirectory } from "../location.js";
|
|
2
|
+
import { CloudAuthorizationRequired } from "../../cloud/login.js";
|
|
3
|
+
import { editInstallationSettings } from "./settings.js";
|
|
4
|
+
import { registerCloudServices } from "../../cloud/registration.js";
|
|
5
|
+
import { registerWithBrowser, registerUnattended } from "./cloud.js";
|
|
6
|
+
import { savedSetup, prepareConfiguration, rejectNewSelections, } from "../configure.js";
|
|
7
|
+
import * as clack from "@clack/prompts";
|
|
8
|
+
import { styleText } from "node:util";
|
|
9
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
10
|
+
import { writeFile, access } from "node:fs/promises";
|
|
11
|
+
import { dirname, resolve, join } from "node:path";
|
|
12
|
+
import { collectInstallation } from "./collect.js";
|
|
13
|
+
import { InstallerCancelled, SectionCancelled, progress, requireTerminal, terminalPrompts, unattendedPrompts, terminalLink, } from "./prompts.js";
|
|
14
|
+
import { saveConfiguration } from "../save.js";
|
|
15
|
+
import { planInstallation, applyInstallation } from "../plan.js";
|
|
16
|
+
import { checkHost, checkNewInstallationPorts, checkInstallationPrerequisites, } from "../prerequisites.js";
|
|
17
|
+
import { startInstallation, withVerifiedAdministrator } from "../lifecycle.js";
|
|
18
|
+
import { administratorSetup } from "../administrator.js";
|
|
19
|
+
import { InstallationError } from "../errors.js";
|
|
20
|
+
const operations = {
|
|
21
|
+
plan: planInstallation,
|
|
22
|
+
host: checkHost,
|
|
23
|
+
ports: checkNewInstallationPorts,
|
|
24
|
+
prerequisites: checkInstallationPrerequisites,
|
|
25
|
+
apply: applyInstallation,
|
|
26
|
+
start: startInstallation,
|
|
27
|
+
administrator: administratorSetup,
|
|
28
|
+
register: registerCloudServices,
|
|
29
|
+
};
|
|
30
|
+
export async function installFromAnswers(options, ui, operator = operations, task = async (_message, work) => work(new AbortController().signal, (message) => {
|
|
31
|
+
ui.note(message, "Startup");
|
|
32
|
+
})) {
|
|
33
|
+
if (options.reapply)
|
|
34
|
+
throw new InstallationError("invalid_configuration", "--reapply requires an existing installation.");
|
|
35
|
+
const saved = await savedSetup(options);
|
|
36
|
+
if (saved)
|
|
37
|
+
rejectNewSelections(options);
|
|
38
|
+
await task("Checking this machine", () => operator.host());
|
|
39
|
+
let draft;
|
|
40
|
+
while (!saved) {
|
|
41
|
+
draft = options.nonInteractive
|
|
42
|
+
? await prepareConfiguration(options)
|
|
43
|
+
: await collectInstallation(ui, options, draft);
|
|
44
|
+
await operator.ports(draft.config.exposure);
|
|
45
|
+
if (options.nonInteractive)
|
|
46
|
+
break;
|
|
47
|
+
try {
|
|
48
|
+
if (await ui.confirm(`Install in ${draft.directory}?`, true))
|
|
49
|
+
break;
|
|
50
|
+
return { state: "cancelled" };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (!(error instanceof SectionCancelled))
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const setup = saved ??
|
|
58
|
+
(draft
|
|
59
|
+
? {
|
|
60
|
+
configFile: await saveConfiguration(draft.directory, draft.config, draft.inputs),
|
|
61
|
+
config: draft.config,
|
|
62
|
+
}
|
|
63
|
+
: undefined);
|
|
64
|
+
if (!setup)
|
|
65
|
+
throw new InstallationError("invalid_configuration", "No installation settings were collected.");
|
|
66
|
+
const { configFile, config } = setup;
|
|
67
|
+
const directory = dirname(configFile);
|
|
68
|
+
const planFile = join(directory, "preview.json");
|
|
69
|
+
const stateDirectory = resolve(directory, config.stateDirectory);
|
|
70
|
+
try {
|
|
71
|
+
await task("Preparing required software", (signal, report) => operator.prerequisites(configFile, { acquire: true, signal, report }));
|
|
72
|
+
if (options.nonInteractive)
|
|
73
|
+
await registerUnattended(configFile, options.cloudCredentialFile, operator.register);
|
|
74
|
+
else
|
|
75
|
+
await registerWithBrowser(configFile, ui, task, operator.register);
|
|
76
|
+
const plan = await task("Checking installation settings", () => operator.plan(configFile));
|
|
77
|
+
await writeFile(planFile, JSON.stringify(plan, null, 2) + "\n", {
|
|
78
|
+
mode: 0o600,
|
|
79
|
+
});
|
|
80
|
+
await task("Installing ClawScarf", () => operator.apply(configFile, planFile));
|
|
81
|
+
if (!(options.start ??
|
|
82
|
+
(options.nonInteractive ? true : await ui.confirm("Start now?", true)))) {
|
|
83
|
+
ui.note(`clawscarf start --directory ${quote(directory)}`, "Start later");
|
|
84
|
+
return { state: "prepared", directory };
|
|
85
|
+
}
|
|
86
|
+
const started = await task("Starting ClawScarf", (signal, report) => operator.start(stateDirectory, report, signal));
|
|
87
|
+
const administrator = await finishAdministrator(stateDirectory, options, ui, task, operator.administrator, true);
|
|
88
|
+
if (administrator)
|
|
89
|
+
return {
|
|
90
|
+
state: "action_required",
|
|
91
|
+
ready: false,
|
|
92
|
+
...administrator,
|
|
93
|
+
directory,
|
|
94
|
+
resume: `clawscarf status --directory ${quote(directory)} --json`,
|
|
95
|
+
};
|
|
96
|
+
const origin = config.exposure.mode === "https"
|
|
97
|
+
? config.exposure.applicationOrigin
|
|
98
|
+
: `http://127.0.0.1:${String(config.exposure.applicationPort)}`;
|
|
99
|
+
return {
|
|
100
|
+
...withVerifiedAdministrator(started),
|
|
101
|
+
directory,
|
|
102
|
+
applicationUrl: origin,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof CloudAuthorizationRequired)
|
|
107
|
+
return {
|
|
108
|
+
state: "action_required",
|
|
109
|
+
action: "cloud_authorization",
|
|
110
|
+
...error.action,
|
|
111
|
+
directory,
|
|
112
|
+
resume: `clawscarf configure --directory ${quote(directory)} --non-interactive${options.start === false ? " --no-start" : ""} --json`,
|
|
113
|
+
};
|
|
114
|
+
if (error instanceof InstallerCancelled ||
|
|
115
|
+
error instanceof SectionCancelled)
|
|
116
|
+
throw error;
|
|
117
|
+
ui.note(`Resume: clawscarf configure --directory ${quote(directory)}\nStatus: clawscarf status --directory ${quote(directory)}\nLogs: clawscarf logs --directory ${quote(directory)} --service controller\nThe server, if started, keeps running. No failed operation is automatically repeated.`, "Installation needs attention");
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function finishAdministrator(state, options, ui, task, administrator = administratorSetup, verify = false) {
|
|
122
|
+
const current = await task("Verifying administrator access", () => administrator(state, false, verify));
|
|
123
|
+
if (current.complete)
|
|
124
|
+
return undefined;
|
|
125
|
+
if (options.nonInteractive)
|
|
126
|
+
return {
|
|
127
|
+
action: "administrator_sign_in",
|
|
128
|
+
...(await administrator(state, true)),
|
|
129
|
+
};
|
|
130
|
+
await browserSignIn(ui, task, () => administrator(state, true), () => administrator(state));
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
async function browserSignIn(ui, task, issue, observe) {
|
|
134
|
+
for (;;) {
|
|
135
|
+
const link = await issue();
|
|
136
|
+
if (!link.url || !link.expiresAt)
|
|
137
|
+
throw new InstallationError("unavailable", "Administrator setup did not return a login link.");
|
|
138
|
+
ui.note(`ClawScarf has started. Open this private link in your browser to sign in, then return here.\n\n${terminalLink(link.url)}\n\nExpires at ${link.expiresAt}.`, styleText(["bold", "yellow"], "ACTION REQUIRED — Administrator sign-in"));
|
|
139
|
+
await ui.openBrowser(link.url);
|
|
140
|
+
const complete = await task("Waiting for administrator sign-in", async (signal) => {
|
|
141
|
+
for (;;) {
|
|
142
|
+
signal.throwIfAborted();
|
|
143
|
+
const status = await observe();
|
|
144
|
+
if (status.complete)
|
|
145
|
+
return true;
|
|
146
|
+
if (!status.expiresAt || Date.now() >= Date.parse(status.expiresAt))
|
|
147
|
+
return false;
|
|
148
|
+
await delay(3000, undefined, { signal }).catch((error) => {
|
|
149
|
+
signal.throwIfAborted();
|
|
150
|
+
throw error;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
if (complete)
|
|
155
|
+
return;
|
|
156
|
+
if (!(await ui.confirm("Login link expired. Create a new link?", true)))
|
|
157
|
+
throw new InstallerCancelled();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function quote(value) {
|
|
161
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
162
|
+
}
|
|
163
|
+
/** The command selects the workflow; users never manipulate preparation files. */
|
|
164
|
+
export async function runConfiguration(options) {
|
|
165
|
+
options = {
|
|
166
|
+
...options,
|
|
167
|
+
directory: options.directory ?? defaultInstallationDirectory,
|
|
168
|
+
};
|
|
169
|
+
if (options.json && !options.nonInteractive)
|
|
170
|
+
throw new InstallationError("invalid_configuration", "Use --non-interactive with --json.");
|
|
171
|
+
if (!options.nonInteractive) {
|
|
172
|
+
requireTerminal();
|
|
173
|
+
clack.intro("ClawScarf — protected OpenClaw for your team");
|
|
174
|
+
}
|
|
175
|
+
const ui = options.nonInteractive ? unattendedPrompts : terminalPrompts;
|
|
176
|
+
try {
|
|
177
|
+
if (options.reapply)
|
|
178
|
+
throw new InstallationError("invalid_configuration", "--reapply requires an existing installation.");
|
|
179
|
+
const saved = await savedSetup(options);
|
|
180
|
+
const state = saved
|
|
181
|
+
? resolve(dirname(saved.configFile), saved.config.stateDirectory)
|
|
182
|
+
: undefined;
|
|
183
|
+
let existing = false;
|
|
184
|
+
if (state) {
|
|
185
|
+
try {
|
|
186
|
+
await access(join(state, "settings.json"));
|
|
187
|
+
existing = true;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (!(error instanceof Error &&
|
|
191
|
+
"code" in error &&
|
|
192
|
+
error.code === "ENOENT"))
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
let result = existing && state
|
|
197
|
+
? await editInstallationSettings(state, ui, options)
|
|
198
|
+
: await installFromAnswers(options, ui, operations, (message, work) => progress(message, work, options));
|
|
199
|
+
if (existing && state && "services" in result) {
|
|
200
|
+
const administrator = await finishAdministrator(state, options, ui, (message, work) => progress(message, work, options));
|
|
201
|
+
if (administrator)
|
|
202
|
+
return {
|
|
203
|
+
state: "action_required",
|
|
204
|
+
ready: false,
|
|
205
|
+
...administrator,
|
|
206
|
+
directory: options.directory,
|
|
207
|
+
resume: `clawscarf status --directory ${quote(resolve(options.directory ?? "."))} --json`,
|
|
208
|
+
};
|
|
209
|
+
result = withVerifiedAdministrator(result);
|
|
210
|
+
}
|
|
211
|
+
if (!options.nonInteractive) {
|
|
212
|
+
const applicationUrl = "applicationUrl" in result && typeof result.applicationUrl === "string"
|
|
213
|
+
? result.applicationUrl
|
|
214
|
+
: saved
|
|
215
|
+
? saved.config.exposure.mode === "https"
|
|
216
|
+
? saved.config.exposure.applicationOrigin
|
|
217
|
+
: `http://127.0.0.1:${String(saved.config.exposure.applicationPort)}`
|
|
218
|
+
: undefined;
|
|
219
|
+
if (result.state === "cancelled")
|
|
220
|
+
clack.cancel("Cancelled.");
|
|
221
|
+
else
|
|
222
|
+
clack.outro("ready" in result
|
|
223
|
+
? result.ready
|
|
224
|
+
? `ClawScarf is ready.${applicationUrl ? ` Visit ${terminalLink(applicationUrl)}` : ""}\n\nManage this installation:\n clawscarf status --directory ${quote(options.directory ?? defaultInstallationDirectory)}\n clawscarf stop --directory ${quote(options.directory ?? defaultInstallationDirectory)}`
|
|
225
|
+
: "ClawScarf needs attention. Check status for details."
|
|
226
|
+
: result.state === "unchanged"
|
|
227
|
+
? "No changes. Server left as it was."
|
|
228
|
+
: "Configuration saved. Server stopped.");
|
|
229
|
+
if ("ready" in result && result.ready && applicationUrl)
|
|
230
|
+
await ui.openBrowser(applicationUrl);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error instanceof CloudAuthorizationRequired)
|
|
236
|
+
return {
|
|
237
|
+
state: "action_required",
|
|
238
|
+
action: "cloud_authorization",
|
|
239
|
+
...error.action,
|
|
240
|
+
directory: options.directory,
|
|
241
|
+
resume: `clawscarf configure --directory ${quote(resolve(options.directory ?? "."))} --non-interactive --yes${options.start === false ? " --no-start" : ""} --json`,
|
|
242
|
+
};
|
|
243
|
+
if (!(error instanceof InstallerCancelled) &&
|
|
244
|
+
!(error instanceof SectionCancelled))
|
|
245
|
+
throw error;
|
|
246
|
+
if (!options.nonInteractive)
|
|
247
|
+
clack.cancel("Exited. Saved files and any running installation are retained.");
|
|
248
|
+
process.exitCode = 130;
|
|
249
|
+
return { state: "cancelled" };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { InstallationError } from "../errors.js";
|
|
2
|
+
import { inputFile } from "./inputs.js";
|
|
3
|
+
export async function secretInput(ui, inputs, message, name, current) {
|
|
4
|
+
const mode = await ui.select(message, [
|
|
5
|
+
...(current ? [{ value: "keep", label: "Keep current value" }] : []),
|
|
6
|
+
{ value: "paste", label: "Paste secret (hidden)" },
|
|
7
|
+
{ value: "file", label: "Import private file" },
|
|
8
|
+
], current ? "keep" : "paste");
|
|
9
|
+
if (mode === "keep" && current)
|
|
10
|
+
return current;
|
|
11
|
+
if (mode === "file")
|
|
12
|
+
return inputFile(ui, `${message} file`, true);
|
|
13
|
+
if (mode !== "paste")
|
|
14
|
+
throw new InstallationError("invalid_configuration", "Select a secret input method.");
|
|
15
|
+
return inputs.set(name, await ui.password(message));
|
|
16
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defaultCloudUrl } from "../../../cloud/url.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { field, inputFile } from "../inputs.js";
|
|
4
|
+
export async function collectAccess(ui, current, cloudUrl = defaultCloudUrl) {
|
|
5
|
+
const custom = await ui.confirm("Use your own OIDC provider?", current.access.mode === "oidc");
|
|
6
|
+
if (!custom)
|
|
7
|
+
return {
|
|
8
|
+
access: {
|
|
9
|
+
mode: "hosted",
|
|
10
|
+
administratorName: current.access.administratorName,
|
|
11
|
+
registrationFile: "./secrets/hosted-login.json",
|
|
12
|
+
cloudUrl: current.access.mode === "hosted" ? current.access.cloudUrl : cloudUrl,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
const previous = current.access.mode === "oidc" ? current.access : undefined;
|
|
16
|
+
const origin = current.exposure.mode === "https"
|
|
17
|
+
? current.exposure.applicationOrigin
|
|
18
|
+
: `http://127.0.0.1:${String(current.exposure.applicationPort)}`;
|
|
19
|
+
ui.note(`Callback: ${origin}/_clawscarf/callback\nAfter logout: ${origin}/_clawscarf/signed-out`, "OIDC application URLs");
|
|
20
|
+
const issuer = await field(ui, "OIDC issuer", z.url(), previous?.issuer);
|
|
21
|
+
const clientId = await ui.text("OIDC client ID", previous?.clientId);
|
|
22
|
+
return {
|
|
23
|
+
access: {
|
|
24
|
+
mode: "oidc",
|
|
25
|
+
administratorName: current.access.administratorName,
|
|
26
|
+
issuer,
|
|
27
|
+
clientId,
|
|
28
|
+
clientSecretFile: previous?.issuer === issuer && previous.clientId === clientId
|
|
29
|
+
? previous.clientSecretFile
|
|
30
|
+
: "",
|
|
31
|
+
...(previous?.issuer === issuer &&
|
|
32
|
+
previous.clientId === clientId &&
|
|
33
|
+
previous.administratorSubject &&
|
|
34
|
+
previous.administratorEmail
|
|
35
|
+
? {
|
|
36
|
+
administratorSubject: previous.administratorSubject,
|
|
37
|
+
administratorEmail: previous.administratorEmail,
|
|
38
|
+
}
|
|
39
|
+
: {}),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export async function collectExposure(ui, current) {
|
|
44
|
+
const network = await ui.confirm("Make this server available over HTTPS?", current.exposure.mode === "https");
|
|
45
|
+
if (!network) {
|
|
46
|
+
const port = z
|
|
47
|
+
.string()
|
|
48
|
+
.regex(/^\d+$/)
|
|
49
|
+
.refine((value) => Number(value) >= 1024 && Number(value) <= 65535, "Use a port from 1024 to 65535.");
|
|
50
|
+
const previous = current.exposure.mode === "local" ? current.exposure : undefined;
|
|
51
|
+
const applicationPort = Number(await field(ui, "Application port", port, String(previous?.applicationPort ?? 18800)));
|
|
52
|
+
const widgetPort = Number(await field(ui, "Widgets port", port.refine((value) => Number(value) !== applicationPort, "Use a different port."), String(previous?.widgetPort ?? 18802)));
|
|
53
|
+
return { mode: "local", applicationPort, widgetPort };
|
|
54
|
+
}
|
|
55
|
+
const previous = current.exposure.mode === "https" ? current.exposure : undefined;
|
|
56
|
+
const origin = z
|
|
57
|
+
.url()
|
|
58
|
+
.refine((value) => new URL(value).origin === value && new URL(value).protocol === "https:", "Use an HTTPS origin.");
|
|
59
|
+
const applicationOrigin = await field(ui, "Application HTTPS origin", origin, previous?.applicationOrigin);
|
|
60
|
+
const widgetOrigin = await field(ui, "Widgets HTTPS origin", origin.refine((value) => new URL(value).port !== new URL(applicationOrigin).port, "Use distinct listener ports."), previous?.widgetOrigin);
|
|
61
|
+
const certificateFile = await inputFile(ui, "TLS certificate file", false, previous?.certificateFile);
|
|
62
|
+
return {
|
|
63
|
+
mode: "https",
|
|
64
|
+
applicationOrigin,
|
|
65
|
+
widgetOrigin,
|
|
66
|
+
certificateFile,
|
|
67
|
+
keyFile: previous?.certificateFile === certificateFile ? previous.keyFile : "",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { inputFile } from "../inputs.js";
|
|
2
|
+
export async function optionalCa(ui, current) {
|
|
3
|
+
if (current) {
|
|
4
|
+
const action = await ui.select("Certificate authority", [
|
|
5
|
+
{ value: "keep", label: "Keep custom certificate authority" },
|
|
6
|
+
{ value: "replace", label: "Replace certificate authority" },
|
|
7
|
+
{ value: "system", label: "Use system certificate authorities" },
|
|
8
|
+
], "keep");
|
|
9
|
+
if (action === "keep")
|
|
10
|
+
return { caFile: current };
|
|
11
|
+
if (action === "system")
|
|
12
|
+
return {};
|
|
13
|
+
return { caFile: await inputFile(ui, "CA certificate file") };
|
|
14
|
+
}
|
|
15
|
+
return (await ui.confirm("Use a custom certificate authority?"))
|
|
16
|
+
? { caFile: await inputFile(ui, "CA certificate file") }
|
|
17
|
+
: {};
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { configurationSchema, gatewayRoutesSchema, } from "../../../models/configuration.js";
|
|
3
|
+
import { InstallationError } from "../../errors.js";
|
|
4
|
+
import { field } from "../inputs.js";
|
|
5
|
+
import { optionalCa } from "./certificates.js";
|
|
6
|
+
export async function collectExternalModels(ui, inputs, file, current) {
|
|
7
|
+
const catalog = z
|
|
8
|
+
.union([gatewayRoutesSchema, configurationSchema])
|
|
9
|
+
.parse(await inputs.readJson(file));
|
|
10
|
+
if ("mode" in catalog && catalog.mode === "disabled")
|
|
11
|
+
throw new InstallationError("invalid_configuration", "LiteLLM requires an enabled model catalog.");
|
|
12
|
+
const baseUrl = await field(ui, "LiteLLM API URL (ending in /v1)", z.url(), "baseUrl" in catalog ? catalog.baseUrl : undefined);
|
|
13
|
+
const config = configurationSchema.parse({
|
|
14
|
+
mode: "external",
|
|
15
|
+
baseUrl,
|
|
16
|
+
models: catalog.models,
|
|
17
|
+
defaultModel: catalog.defaultModel,
|
|
18
|
+
...(catalog.thinkingDefault
|
|
19
|
+
? { thinkingDefault: catalog.thinkingDefault }
|
|
20
|
+
: {}),
|
|
21
|
+
});
|
|
22
|
+
const previous = current?.mode === "external"
|
|
23
|
+
? configurationSchema.parse(await inputs.readJson(current.configurationFile))
|
|
24
|
+
: undefined;
|
|
25
|
+
return {
|
|
26
|
+
mode: "external",
|
|
27
|
+
configurationFile: inputs.set("external-models.json", JSON.stringify(config, null, 2)),
|
|
28
|
+
credentialFile: current?.mode === "external" &&
|
|
29
|
+
previous?.mode === "external" &&
|
|
30
|
+
previous.baseUrl === baseUrl
|
|
31
|
+
? current.credentialFile
|
|
32
|
+
: "",
|
|
33
|
+
...(await optionalCa(ui, current?.mode === "external" ? current.caFile : undefined)),
|
|
34
|
+
};
|
|
35
|
+
}
|