#!/usr/bin/env node import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs" import { execFileSync } from "node:child_process" import { createHash, generateKeyPairSync } from "node:crypto" import { tmpdir } from "node:os" import { dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" const VERSION = "0.1.0" // The installed command name. Keep it explicit so setup guides can distinguish // bootstrap actions from other OpenWork commands a user may already have. const COMMAND_NAME = "openwork-bootstrap" const DEFAULT_OPENWORK_MARKETPLACE_NAME = "OpenWork Marketplace" const executableBasename = () => (process.platform === "win32" ? `${COMMAND_NAME}.cmd` : COMMAND_NAME) const here = dirname(fileURLToPath(import.meta.url)) const selfPath = fileURLToPath(import.meta.url) function parseArgs(argv) { const positionals = [] const flags = new Map() for (let index = 0; index < argv.length; index += 1) { const arg = argv[index] if (!arg.startsWith("--")) { positionals.push(arg) continue } const raw = arg.slice(2) const eq = raw.indexOf("=") if (eq >= 0) { flags.set(raw.slice(0, eq), raw.slice(eq + 1)) continue } const next = argv[index + 1] if (next && !next.startsWith("--")) { flags.set(raw, next) index += 1 } else { flags.set(raw, true) } } return { positionals, flags } } function getFlag(flags, name, fallback = undefined) { const value = flags.get(name) return value === undefined || value === true ? fallback : String(value) } function hasFlag(flags, name) { return flags.get(name) === true || flags.get(name) === "true" } function jsonOut(value, json) { if (json) { console.log(JSON.stringify(value, null, 2)) } else if (value.message) { console.log(value.message) } else { console.log(JSON.stringify(value, null, 2)) } } function printHelp() { console.log([ "openwork-bootstrap", "", "Usage:", " openwork-bootstrap install [--bin-dir ] [--install-dir ] [--source ] [--json]", " openwork-bootstrap install app --manifest [--app-dir ] [--json]", " openwork-bootstrap doctor [--bin-dir ] [--install-dir ] [--base-url ] [--desktop-bootstrap] [--json]", " OPENWORK_OWNER_PASSWORD= openwork-bootstrap cloud onboard --base-url --owner-email --org-name --invite-email [--skill-name ] [--web-base-url ] [--prepare-desktop] [--json]", " openwork-bootstrap cloud bootstrap-workspace --base-url --workspace-name [--skill-name ] [--owner-email ] [--teammate-emails a@x.com,b@y.com] [--claim-roles owner,member] [--web-base-url ] [--prepare-desktop] [--json]", " openwork-bootstrap cloud claim-link [--role owner] [--desktop-bootstrap-path ] [--json]", "", "Commands:", " install Install the openwork-bootstrap CLI into a user bin dir", " install app Download and install the desktop app from a manifest", " doctor Check CLI installation and optional Den API health", " cloud onboard Sign up, create an org, invite a teammate, and create a skill", " cloud bootstrap-workspace Create a provisional workspace without email/password auth", " cloud claim-link Retrieve a claim link saved by --prepare-desktop. Only run", " this when you are ready to hand the link to a human; do", " not print claim links preemptively.", "", "Options:", " --web-base-url Browser-facing origin written into --prepare-desktop's", " config (used for the app's Sign In button and claim", " links). Defaults to https://app.openworklabs.com when", " --base-url is the hosted API (api.openworklabs.com);", " set explicitly for self-hosted/custom deployments.", " --json Print machine-readable JSON", " --version Print version", " --help Show help", ].join("\n")) } async function readStdin() { const chunks = [] for await (const chunk of process.stdin) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) } return Buffer.concat(chunks).toString("utf8") } function defaultInstallDir() { return process.env.OPENWORK_INSTALL_DIR || join(process.env.HOME || process.cwd(), ".openwork", "bootstrap") } function defaultBinDir() { return process.env.OPENWORK_BIN_DIR || join(process.env.HOME || process.cwd(), ".local", "bin") } function defaultAppDir() { return process.env.OPENWORK_APP_DIR || (process.platform === "darwin" ? join(process.env.HOME || process.cwd(), "Applications") : process.platform === "win32" ? join(process.env.LOCALAPPDATA || join(process.env.HOME || process.cwd(), "AppData", "Local"), "OpenWork") : join(process.env.HOME || process.cwd(), ".local", "share", "openwork")) } function configHomeDir() { if (process.env.XDG_CONFIG_HOME) return process.env.XDG_CONFIG_HOME if (process.platform === "win32") { // Match the Electron shell (apps/desktop/electron/workspace-store.mjs): // LOCALAPPDATA, then the conventional Local dir — never ~/.config on Windows. if (process.env.LOCALAPPDATA) return process.env.LOCALAPPDATA return join(process.env.USERPROFILE || process.env.HOME || process.cwd(), "AppData", "Local") } return join(process.env.HOME || process.cwd(), ".config") } function defaultDesktopBootstrapPath() { return process.env.OPENWORK_DESKTOP_BOOTSTRAP_PATH || join(configHomeDir(), "openwork", "desktop-bootstrap.json") } function defaultSkillsDir() { return process.env.OPENWORK_SKILLS_DIR || join(configHomeDir(), "opencode", "skills") } function defaultDeviceKeyPath() { return process.env.OPENWORK_DEVICE_KEY_PATH || join(configHomeDir(), "openwork", "bootstrap-device-key.json") } // The desktop app's `desktop-bootstrap.json` `baseUrl` field is the WEB origin // it opens in the user's browser for sign-in (e.g. for "Sign in" and claim // links) - it is a different host than the API origin used for CLI/API calls // (`--base-url`, `apiBaseUrl`). Reusing the API host here breaks sign-in: the // browser opens `https://api.openworklabs.com/?mode=sign-in...` and shows raw // API JSON instead of the sign-in page. Derive the correct web host instead // of assuming it equals the API host. function deriveWebBaseUrl(apiBaseUrl) { try { const url = new URL(apiBaseUrl) if (url.hostname === "api.openworklabs.com") { return "https://app.openworklabs.com" } // Local/self-hosted dev: den-web commonly proxies the API at a different // port on the same host (see ee/apps/den-web's /api/den proxy). Callers // that need this to be exact should pass --web-base-url explicitly. return apiBaseUrl } catch { return apiBaseUrl } } function slugifySkillName(value) { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "openwork-bootstrap-skill" } function runInstall(args) { if (args.positionals[1] === "app") { return runInstallApp(args) } const installDir = resolve(getFlag(args.flags, "install-dir", defaultInstallDir())) const binDir = resolve(getFlag(args.flags, "bin-dir", defaultBinDir())) const source = resolve(getFlag(args.flags, "source", selfPath)) const json = hasFlag(args.flags, "json") if (!existsSync(source)) { throw new Error(`source_not_found: ${source}`) } mkdirSync(installDir, { recursive: true }) mkdirSync(binDir, { recursive: true }) const installedCli = join(installDir, "openwork.mjs") copyFileSync(source, installedCli) chmodSync(installedCli, 0o755) const executable = join(binDir, executableBasename()) if (process.platform === "win32") { writeFileSync(executable, `@echo off\r\nnode "${installedCli}" %*\r\n`) } else { writeFileSync(executable, `#!/usr/bin/env sh\nexec node "${installedCli}" "$@"\n`) } chmodSync(executable, 0o755) const manifest = { version: VERSION, installedAt: new Date().toISOString(), installDir, binDir, executable, cli: installedCli, } writeFileSync(join(installDir, "install.json"), JSON.stringify(manifest, null, 2)) jsonOut({ ok: true, message: `OpenWork CLI installed at ${executable}`, install: manifest }, json) } function sha256(buffer) { return createHash("sha256").update(buffer).digest("hex") } function isHttpUrl(value) { return /^https?:\/\//i.test(value) } function filePathFromUrl(value) { if (value.startsWith("file://")) { return fileURLToPath(value) } return resolve(value) } async function readJsonLocation(location) { if (isHttpUrl(location)) { const response = await fetch(location) if (!response.ok) throw new Error(`manifest_fetch_failed: ${response.status}`) return response.json() } return JSON.parse(readFileSync(filePathFromUrl(location), "utf8")) } async function downloadArtifact(url, destination) { if (isHttpUrl(url)) { const response = await fetch(url) if (!response.ok) throw new Error(`artifact_download_failed: ${response.status}`) writeFileSync(destination, Buffer.from(await response.arrayBuffer())) return } copyFileSync(filePathFromUrl(url), destination) } function selectArtifact(manifest) { const platform = process.platform const arch = process.arch const candidates = [ manifest.artifacts?.[platform]?.[arch], manifest.artifacts?.[`${platform}-${arch}`], manifest.artifacts?.[platform], Array.isArray(manifest.artifacts) ? manifest.artifacts.find((artifact) => artifact.platform === platform && (!artifact.arch || artifact.arch === arch)) : null, ].filter(Boolean) const artifact = candidates[0] if (!artifact?.url) throw new Error(`no_artifact_for_platform: ${platform}-${arch}`) return { ...artifact, platform, arch } } function inferArtifactType(url) { const lower = url.toLowerCase() if (lower.endsWith(".dmg")) return "dmg" if (lower.endsWith(".zip")) return "zip" if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) return "tar.gz" if (lower.endsWith(".appimage")) return "appimage" if (lower.endsWith(".exe")) return "exe" if (lower.endsWith(".msi")) return "msi" return null } function defaultInstalledName(type, manifest, artifact) { if (artifact.appName || manifest.appName) return artifact.appName || manifest.appName if (type === "dmg") return "OpenWork.app" if (type === "appimage") return "OpenWork.AppImage" if (type === "exe") return "OpenWork.exe" if (type === "msi") return "OpenWork.msi" if (process.platform === "darwin") return "OpenWork.app" if (process.platform === "win32") return "OpenWork.exe" return "openwork" } function findInstallCandidate(root, expectedName) { const direct = join(root, expectedName) if (existsSync(direct)) return direct const queue = [root] while (queue.length > 0) { const current = queue.shift() for (const entry of readdirSync(current, { withFileTypes: true })) { const path = join(current, entry.name) if (entry.name === expectedName) return path if (entry.isDirectory()) queue.push(path) } } throw new Error(`app_not_found_in_archive: ${expectedName}`) } // Copy an installed artifact into place. macOS .app bundles contain internal // framework symlinks (e.g. Versions/Current, the framework binary/Resources // links) that a naive recursive copy can break — leaving dangling links into a // now-unmounted DMG, which makes Gatekeeper report the app as "damaged". Use // `ditto` on macOS, which is the Apple-supported way to copy bundles while // preserving relative symlinks and the code signature. function copyArtifact(source, target) { if (process.platform === "darwin") { try { execFileSync("ditto", [source, target], { stdio: "pipe" }) } catch { // Fall back to cp -R (also preserves bundle symlinks) before giving up. execFileSync("cp", ["-R", source, target], { stdio: "pipe" }) } // Remove the quarantine flag so Gatekeeper does not block the freshly // installed (already-notarized) app on first launch. Best-effort. try { execFileSync("xattr", ["-dr", "com.apple.quarantine", target], { stdio: "pipe" }) } catch {} return } cpSync(source, target, { recursive: true }) } function installFromDirectory(input) { const source = findInstallCandidate(input.sourceDir, input.appName) mkdirSync(input.appDir, { recursive: true }) const target = join(input.appDir, input.appName) rmSync(target, { recursive: true, force: true }) copyArtifact(source, target) if (input.executable) chmodSync(target, 0o755) return target } function installDmg(input) { if (process.platform !== "darwin") { throw new Error("dmg_install_requires_macos") } const mountPoint = join(input.workDir, "mount") mkdirSync(mountPoint, { recursive: true }) let mounted = false try { execFileSync("hdiutil", ["attach", input.artifactPath, "-nobrowse", "-readonly", "-mountpoint", mountPoint], { stdio: "pipe" }) mounted = true const appName = input.appName || "OpenWork.app" const sourceApp = join(mountPoint, appName) if (!existsSync(sourceApp)) { throw new Error(`app_not_found_in_dmg: ${appName}`) } mkdirSync(input.appDir, { recursive: true }) const targetApp = join(input.appDir, appName) rmSync(targetApp, { recursive: true, force: true }) copyArtifact(sourceApp, targetApp) return targetApp } finally { if (mounted) { try { execFileSync("hdiutil", ["detach", mountPoint, "-quiet"], { stdio: "pipe" }) } catch { execFileSync("hdiutil", ["detach", mountPoint, "-force", "-quiet"], { stdio: "pipe" }) } } } } function installZip(input) { const extractDir = join(input.workDir, "zip") mkdirSync(extractDir, { recursive: true }) if (process.platform === "win32") { execFileSync("powershell.exe", ["-NoProfile", "-Command", `Expand-Archive -LiteralPath ${JSON.stringify(input.artifactPath)} -DestinationPath ${JSON.stringify(extractDir)} -Force`], { stdio: "pipe" }) } else if (process.platform === "darwin") { execFileSync("ditto", ["-x", "-k", input.artifactPath, extractDir], { stdio: "pipe" }) } else { execFileSync("unzip", ["-q", input.artifactPath, "-d", extractDir], { stdio: "pipe" }) } return installFromDirectory({ ...input, sourceDir: extractDir }) } function installTarGz(input) { const extractDir = join(input.workDir, "tar") mkdirSync(extractDir, { recursive: true }) execFileSync("tar", ["-xzf", input.artifactPath, "-C", extractDir], { stdio: "pipe" }) return installFromDirectory({ ...input, sourceDir: extractDir }) } function installSingleFile(input) { mkdirSync(input.appDir, { recursive: true }) const target = join(input.appDir, input.appName) rmSync(target, { force: true }) copyFileSync(input.artifactPath, target) if (input.executable) chmodSync(target, 0o755) return target } async function runInstallApp(args) { const json = hasFlag(args.flags, "json") const manifestLocation = getFlag(args.flags, "manifest") || process.env.OPENWORK_INSTALL_MANIFEST if (!manifestLocation) throw new Error("missing_required_flag: --manifest") const appDir = resolve(getFlag(args.flags, "app-dir", defaultAppDir())) const workDir = join(tmpdir(), `openwork-app-install-${Date.now()}-${Math.random().toString(36).slice(2)}`) mkdirSync(workDir, { recursive: true }) try { const manifest = await readJsonLocation(manifestLocation) const artifact = selectArtifact(manifest) const type = artifact.type || inferArtifactType(artifact.url) if (!type) throw new Error("unsupported_app_artifact_type: unknown") const artifactPath = join(workDir, artifact.fileName || "OpenWork.dmg") await downloadArtifact(artifact.url, artifactPath) const digest = sha256(readFileSync(artifactPath)) if (artifact.sha256 && digest !== artifact.sha256) { throw new Error(`checksum_mismatch: expected ${artifact.sha256} got ${digest}`) } const appName = defaultInstalledName(type, manifest, artifact) const appPath = type === "dmg" ? installDmg({ artifactPath, workDir, appDir, appName }) : type === "zip" ? installZip({ artifactPath, workDir, appDir, appName, executable: !appName.endsWith(".app") && process.platform !== "win32" }) : type === "tar.gz" ? installTarGz({ artifactPath, workDir, appDir, appName, executable: process.platform !== "win32" }) : type === "appimage" ? installSingleFile({ artifactPath, appDir, appName, executable: true }) : type === "exe" || type === "msi" ? installSingleFile({ artifactPath, appDir, appName, executable: false }) : (() => { throw new Error(`unsupported_app_artifact_type: ${type}`) })() const install = { version: manifest.version || artifact.version || null, installedAt: new Date().toISOString(), appDir, appPath, manifest: manifestLocation, artifact: { type, url: artifact.url, sha256: digest, platform: artifact.platform, arch: artifact.arch, }, } mkdirSync(dirname(appPath), { recursive: true }) writeFileSync(join(appDir, "openwork-app-install.json"), JSON.stringify(install, null, 2)) jsonOut({ ok: true, message: `OpenWork app installed at ${appPath}`, install }, json) } finally { rmSync(workDir, { recursive: true, force: true }) } } async function runDoctor(args) { const installDir = resolve(getFlag(args.flags, "install-dir", defaultInstallDir())) const binDir = resolve(getFlag(args.flags, "bin-dir", defaultBinDir())) const baseUrl = getFlag(args.flags, "base-url") const appDir = resolve(getFlag(args.flags, "app-dir", defaultAppDir())) const desktopBootstrapPath = resolve(getFlag(args.flags, "desktop-bootstrap-path", defaultDesktopBootstrapPath())) const json = hasFlag(args.flags, "json") const checks = [] checks.push({ name: "node", ok: Number(process.versions.node.split(".")[0]) >= 20, value: process.versions.node }) checks.push({ name: "installDir", ok: existsSync(installDir), value: installDir }) checks.push({ name: "binDir", ok: existsSync(binDir), value: binDir }) const executable = join(binDir, executableBasename()) const executableOk = existsSync(executable) && statSync(executable).isFile() checks.push({ name: "openworkExecutable", ok: executableOk, value: executable }) const manifestPath = join(installDir, "install.json") let manifest = null if (existsSync(manifestPath)) { manifest = JSON.parse(readFileSync(manifestPath, "utf8")) checks.push({ name: "manifest", ok: true, value: manifestPath }) } else { checks.push({ name: "manifest", ok: false, value: manifestPath }) } if (baseUrl) { try { const response = await fetch(`${baseUrl.replace(/\/$/, "")}/health`) const body = await response.json().catch(() => null) checks.push({ name: "denApiHealth", ok: response.ok && body?.ok === true, value: { status: response.status, body } }) } catch (error) { checks.push({ name: "denApiHealth", ok: false, value: error instanceof Error ? error.message : String(error) }) } } if (hasFlag(args.flags, "app") || args.flags.has("app-dir")) { const appManifest = join(appDir, "openwork-app-install.json") let appPath = process.platform === "darwin" ? join(appDir, "OpenWork.app") : process.platform === "win32" ? join(appDir, "OpenWork.exe") : join(appDir, "openwork") if (existsSync(appManifest)) { try { const appInstall = JSON.parse(readFileSync(appManifest, "utf8")) if (appInstall.appPath) appPath = appInstall.appPath } catch { // Keep fallback path. } } checks.push({ name: "openworkApp", ok: existsSync(appPath), value: appPath }) checks.push({ name: "appInstallManifest", ok: existsSync(appManifest), value: appManifest }) } if (hasFlag(args.flags, "desktop-bootstrap") || args.flags.has("desktop-bootstrap-path")) { let bootstrap = null try { bootstrap = JSON.parse(readFileSync(desktopBootstrapPath, "utf8")) } catch { bootstrap = null } const handoff = bootstrap?.handoff const prepared = bootstrap?.prepared checks.push({ name: "desktopBootstrap", ok: Boolean(bootstrap?.baseUrl), value: desktopBootstrapPath }) checks.push({ name: "desktopBootstrapPrepared", ok: Boolean(prepared?.orgId && prepared?.skillId && prepared?.skillPath), value: prepared ? { orgId: prepared.orgId, orgName: prepared.orgName, skillId: prepared.skillId, skillTitle: prepared.skillTitle, skillPath: prepared.skillPath } : null }) checks.push({ name: "desktopBootstrapHandoff", ok: Boolean(handoff?.grant && handoff?.orgId && handoff?.skillId) || Boolean(prepared?.orgId && prepared?.skillId), value: handoff ? { orgId: handoff.orgId, orgName: handoff.orgName, skillId: handoff.skillId, skillTitle: handoff.skillTitle } : "consumed" }) } const ok = checks.every((check) => check.ok) jsonOut({ ok, message: ok ? "OpenWork doctor: ok" : "OpenWork doctor: failed", version: VERSION, manifest, checks }, json) if (!ok) process.exitCode = 1 } async function request(baseUrl, path, options = {}) { const response = await fetch(`${baseUrl}${path}`, { ...options, headers: { "content-type": "application/json", origin: baseUrl, ...(options.headers || {}), }, }) const text = await response.text() let body = null if (text) { try { body = JSON.parse(text) } catch { body = text } } return { status: response.status, body } } async function signupAndSignin(baseUrl, input) { const signup = await request(baseUrl, "/api/auth/sign-up/email", { method: "POST", body: JSON.stringify({ name: input.name, email: input.email, password: input.password }), }) if (signup.status !== 200 && signup.status !== 400) { throw new Error(`signup_failed: ${signup.status} ${JSON.stringify(signup.body)}`) } const signin = await request(baseUrl, "/api/auth/sign-in/email", { method: "POST", body: JSON.stringify({ email: input.email, password: input.password }), }) if (signin.status !== 200 || !signin.body?.token) { throw new Error(`signin_failed: ${signin.status} ${JSON.stringify(signin.body)}`) } return { signup, signin, token: signin.body.token, user: signin.body.user } } function skillText(name, output) { return `---\nname: ${name}\ndescription: Starter skill created by openwork bootstrap.\nopenworkBootstrapTrigger: bootstrap.verify\nopenworkBootstrapOutput: ${JSON.stringify(output)}\n---\n\n# ${name}\n\nWhen triggered with \`bootstrap.verify\`, output exactly:\n\n\`${output}\`\n\nUse this skill to confirm OpenWork cloud onboarding can create and trigger a deterministic skill.` } async function createCloudSkillPlugin(baseUrl, auth, input) { const marketplaces = await request(baseUrl, "/v1/marketplaces", { method: "GET", headers: auth, }) if (marketplaces.status !== 200 || !Array.isArray(marketplaces.body?.items)) { throw new Error(`marketplace_list_failed: ${marketplaces.status} ${JSON.stringify(marketplaces.body)}`) } const marketplace = marketplaces.body.items.find((item) => item?.name === DEFAULT_OPENWORK_MARKETPLACE_NAME) || marketplaces.body.items[0] if (!marketplace?.id) { throw new Error("marketplace_missing: no marketplace available for skill plugin") } const plugin = await request(baseUrl, "/v1/plugins", { method: "POST", headers: auth, body: JSON.stringify({ name: input.name, components: [{ type: "skill", input: { rawSourceText: input.rawSourceText } }], orgWide: true, marketplaceId: marketplace.id, }), }) if (plugin.status !== 201 || !plugin.body?.item?.id) { throw new Error(`plugin_create_failed: ${plugin.status} ${JSON.stringify(plugin.body)}`) } const memberships = await request(baseUrl, `/v1/plugins/${encodeURIComponent(plugin.body.item.id)}/config-objects`, { method: "GET", headers: auth, }) if (memberships.status !== 200 || !Array.isArray(memberships.body?.items)) { throw new Error(`plugin_components_failed: ${memberships.status} ${JSON.stringify(memberships.body)}`) } const skillMembership = memberships.body.items.find((item) => item?.configObject?.objectType === "skill") const configObject = skillMembership?.configObject if (!configObject?.id) { throw new Error(`skill_component_missing: ${JSON.stringify(memberships.body)}`) } return { id: configObject.id, title: configObject.title || input.name, skillText: input.rawSourceText, pluginId: plugin.body.item.id, marketplaceId: marketplace.id, } } function readFrontmatterValue(text, key) { const match = text.match(/^---\n([\s\S]*?)\n---/) if (!match) return null for (const line of match[1].split(/\r?\n/g)) { const index = line.indexOf(":") if (index < 0) continue const name = line.slice(0, index).trim() if (name !== key) continue const raw = line.slice(index + 1).trim() if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) { try { return JSON.parse(raw) } catch { return raw.slice(1, -1) } } return raw } return null } function runBootstrapSkill(skill, input) { const trigger = readFrontmatterValue(skill.skillText, "openworkBootstrapTrigger") const output = readFrontmatterValue(skill.skillText, "openworkBootstrapOutput") const triggered = trigger === input.trigger && typeof output === "string" && output.length > 0 return { triggered, trigger, input, output: triggered ? output : null, skill: { id: skill.id, title: skill.title, }, } } async function createDesktopHandoff(baseUrl, auth) { const handoff = await request(baseUrl, "/v1/auth/desktop-handoff", { method: "POST", headers: auth, body: JSON.stringify({ desktopScheme: "openwork" }), }) if (handoff.status !== 200 || !handoff.body?.grant) { throw new Error(`desktop_handoff_failed: ${handoff.status} ${JSON.stringify(handoff.body)}`) } return handoff.body } function writePreparedDesktop(input) { const bootstrapPath = resolve(input.bootstrapPath) let existingBrandAppName = null if (existsSync(bootstrapPath)) { try { const existingBootstrap = JSON.parse(readFileSync(bootstrapPath, "utf8")) existingBrandAppName = typeof existingBootstrap.brandAppName === "string" ? existingBootstrap.brandAppName.trim().slice(0, 64) : null } catch {} } const skillName = slugifySkillName(input.skill.title) const skillDir = resolve(input.skillsDir, skillName) const skillPath = join(skillDir, "SKILL.md") mkdirSync(dirname(bootstrapPath), { recursive: true }) mkdirSync(skillDir, { recursive: true }) writeFileSync(skillPath, input.skill.skillText, "utf8") const preparedAt = new Date().toISOString() const prepared = { orgId: input.organization.id, orgName: input.organization.name, orgSlug: input.organization.slug, skillId: input.skill.id, skillTitle: input.skill.title, skillsDir: resolve(input.skillsDir), skillPath, preparedAt, } const bootstrap = { baseUrl: input.baseUrl, apiBaseUrl: input.apiBaseUrl, requireSignin: false, ...(existingBrandAppName ? { brandAppName: existingBrandAppName } : {}), prepared, ...(input.claimLinks ? { claimLinks: input.claimLinks } : {}), } if (input.handoff) { bootstrap.handoff = { grant: input.handoff.grant, denBaseUrl: input.baseUrl, orgId: prepared.orgId, orgName: prepared.orgName, orgSlug: prepared.orgSlug, skillId: prepared.skillId, skillTitle: prepared.skillTitle, createdAt: preparedAt, } } else { bootstrap.handoff = null } writeFileSync(bootstrapPath, `${JSON.stringify(bootstrap, null, 2)}\n`, "utf8") return { prepared: true, bootstrapPath, skillsDir: resolve(input.skillsDir), skillPath, ...(input.handoff ? { handoffExpiresAt: input.handoff.expiresAt, handoffGrant: "redacted: saved to bootstrapPath" } : {}), ...(input.claimLinks ? { claimLinks: input.claimLinks.map((link) => ({ id: link.id, role: link.role, expiresAt: link.expiresAt, url: `redacted: run "openwork-bootstrap cloud claim-link --role ${link.role}" to view`, })), } : {}), } } function ensureDeviceKey(filePath) { const keyPath = resolve(filePath) if (existsSync(keyPath)) { const stored = JSON.parse(readFileSync(keyPath, "utf8")) if (typeof stored.publicKey === "string" && stored.publicKey.trim()) { return { path: keyPath, publicKey: stored.publicKey.trim(), reused: true } } } const { publicKey, privateKey } = generateKeyPairSync("ed25519", { publicKeyEncoding: { type: "spki", format: "pem" }, privateKeyEncoding: { type: "pkcs8", format: "pem" }, }) mkdirSync(dirname(keyPath), { recursive: true }) writeFileSync(keyPath, `${JSON.stringify({ publicKey, privateKey, createdAt: new Date().toISOString() }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }) try { chmodSync(keyPath, 0o600) } catch {} return { path: keyPath, publicKey, reused: false } } async function resolveOwnerPassword(flags) { const fromFlag = getFlag(flags, "owner-password") if (fromFlag) return fromFlag const envName = getFlag(flags, "owner-password-env", "OPENWORK_OWNER_PASSWORD") const fromEnv = process.env[envName] if (fromEnv) return fromEnv const filePath = getFlag(flags, "owner-password-file") if (filePath) return readFileSync(resolve(filePath), "utf8").trim() if (hasFlag(flags, "owner-password-stdin")) return (await readStdin()).trim() return null } async function runCloudOnboard(args) { const subcommand = args.positionals[1] if (subcommand !== "onboard") { printHelp() process.exitCode = 1 return } const json = hasFlag(args.flags, "json") const baseUrl = getFlag(args.flags, "base-url")?.replace(/\/$/, "") const ownerEmail = getFlag(args.flags, "owner-email") const ownerPassword = await resolveOwnerPassword(args.flags) const orgName = getFlag(args.flags, "org-name") const inviteEmail = getFlag(args.flags, "invite-email") const skillName = getFlag(args.flags, "skill-name", "First OpenWork Skill") const skillOutput = getFlag(args.flags, "skill-output", "OPENWORK_BOOTSTRAP_SKILL_TRIGGERED") const prepareDesktop = hasFlag(args.flags, "prepare-desktop") const desktopBootstrapPath = getFlag(args.flags, "desktop-bootstrap-path", defaultDesktopBootstrapPath()) const skillsDir = getFlag(args.flags, "skills-dir", defaultSkillsDir()) const webBaseUrl = getFlag(args.flags, "web-base-url", deriveWebBaseUrl(baseUrl))?.replace(/\/$/, "") for (const [name, value] of Object.entries({ baseUrl, ownerEmail, ownerPassword, orgName, inviteEmail })) { if (!value) throw new Error(`missing_required_flag: --${name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`) } const health = await request(baseUrl, "/health", { method: "GET" }) if (health.status !== 200 || health.body?.ok !== true) { throw new Error(`den_api_unhealthy: ${health.status} ${JSON.stringify(health.body)}`) } const owner = await signupAndSignin(baseUrl, { name: "OpenWork Owner", email: ownerEmail, password: ownerPassword, }) const auth = { authorization: `Bearer ${owner.token}` } const org = await request(baseUrl, "/v1/org", { method: "POST", headers: auth, body: JSON.stringify({ name: orgName }), }) if (org.status !== 201 || !org.body?.organization?.id) { throw new Error(`org_create_failed: ${org.status} ${JSON.stringify(org.body)}`) } const invite = await request(baseUrl, "/v1/invitations", { method: "POST", headers: auth, body: JSON.stringify({ email: inviteEmail, role: "member" }), }) if (invite.status !== 201 || !invite.body?.invitationId) { throw new Error(`invite_failed: ${invite.status} ${JSON.stringify(invite.body)}`) } const rawSourceText = skillText(skillName, skillOutput) const skill = await createCloudSkillPlugin(baseUrl, auth, { name: skillName, rawSourceText, }) const skillRun = runBootstrapSkill(skill, { trigger: "bootstrap.verify" }) if (!skillRun.triggered || skillRun.output !== skillOutput) { throw new Error(`skill_trigger_failed: ${JSON.stringify(skillRun)}`) } let desktop = null if (prepareDesktop) { const handoff = await createDesktopHandoff(baseUrl, auth) desktop = writePreparedDesktop({ baseUrl: webBaseUrl, apiBaseUrl: baseUrl, bootstrapPath: desktopBootstrapPath, skillsDir, handoff, organization: org.body.organization, skill, }) } jsonOut({ ok: true, message: "OpenWork cloud onboarding complete", user: { id: owner.user.id, email: owner.user.email, emailVerified: owner.user.emailVerified }, organization: org.body.organization, invitation: invite.body, skill, skillRun, desktop, }, json) } async function runCloudBootstrapWorkspace(args) { const json = hasFlag(args.flags, "json") const baseUrl = getFlag(args.flags, "base-url", "https://api.openworklabs.com")?.replace(/\/$/, "") const workspaceName = getFlag(args.flags, "workspace-name") const skillName = getFlag(args.flags, "skill-name", "First OpenWork Skill") const ownerEmail = getFlag(args.flags, "owner-email") const prepareDesktop = hasFlag(args.flags, "prepare-desktop") const desktopBootstrapPath = getFlag(args.flags, "desktop-bootstrap-path", defaultDesktopBootstrapPath()) const skillsDir = getFlag(args.flags, "skills-dir", defaultSkillsDir()) const deviceKeyPath = getFlag(args.flags, "device-key-path", defaultDeviceKeyPath()) const webBaseUrl = getFlag(args.flags, "web-base-url", deriveWebBaseUrl(baseUrl))?.replace(/\/$/, "") const claimRoles = String(getFlag(args.flags, "claim-roles", "owner")) .split(",") .map((role) => role.trim()) .filter(Boolean) const teammateEmails = String(getFlag(args.flags, "teammate-emails", "")) .split(",") .map((email) => email.trim()) .filter(Boolean) for (const [name, value] of Object.entries({ baseUrl, workspaceName })) { if (!value) throw new Error(`missing_required_flag: --${name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`) } const health = await request(baseUrl, "/health", { method: "GET" }) if (health.status !== 200 || health.body?.ok !== true) { throw new Error(`den_api_unhealthy: ${health.status} ${JSON.stringify(health.body)}`) } const deviceKey = ensureDeviceKey(deviceKeyPath) const response = await request(baseUrl, "/v1/bootstrap/workspace", { method: "POST", body: JSON.stringify({ workspaceName, skillName, devicePublicKey: deviceKey.publicKey, claimRoles, ...(ownerEmail ? { ownerEmail } : {}), ...(teammateEmails.length > 0 ? { teammateEmails } : {}), }), }) if (response.status !== 200 || response.body?.ok !== true || !response.body?.organization?.id || !response.body?.skill?.id) { throw new Error(`workspace_bootstrap_failed: ${response.status} ${JSON.stringify(response.body)}`) } const skill = { ...response.body.skill, skillText: skillText(response.body.skill.title, response.body.skill.output || "OPENWORK_BOOTSTRAP_SKILL_TRIGGERED"), } const skillRun = runBootstrapSkill(skill, { trigger: "bootstrap.verify" }) if (!skillRun.triggered || skillRun.output !== "OPENWORK_BOOTSTRAP_SKILL_TRIGGERED") { throw new Error(`skill_trigger_failed: ${JSON.stringify(skillRun)}`) } let desktop = null if (prepareDesktop) { desktop = writePreparedDesktop({ baseUrl: webBaseUrl, apiBaseUrl: baseUrl, bootstrapPath: desktopBootstrapPath, skillsDir, organization: response.body.organization, skill, claimLinks: response.body.claimLinks, }) } jsonOut({ ok: true, message: "OpenWork workspace bootstrap complete", organization: response.body.organization, setup: response.body.setup, skill: response.body.skill, skillRun, claimLinks: response.body.claimLinks.map((link) => ({ id: link.id, role: link.role, expiresAt: link.expiresAt, url: prepareDesktop ? `redacted: run "openwork-bootstrap cloud claim-link --role ${link.role}" to view` : "discarded: rerun with --prepare-desktop to persist this link, otherwise it cannot be retrieved later", })), device: { publicKeyPath: deviceKey.path, reused: deviceKey.reused }, desktop, }, json) } function runCloudClaimLink(args) { const json = hasFlag(args.flags, "json") const desktopBootstrapPath = resolve(getFlag(args.flags, "desktop-bootstrap-path", defaultDesktopBootstrapPath())) const roleFilter = getFlag(args.flags, "role") if (!existsSync(desktopBootstrapPath)) { throw new Error(`desktop_bootstrap_not_found: ${desktopBootstrapPath} (run cloud bootstrap-workspace --prepare-desktop first)`) } const bootstrap = JSON.parse(readFileSync(desktopBootstrapPath, "utf8")) const allLinks = Array.isArray(bootstrap.claimLinks) ? bootstrap.claimLinks : [] const claimLinks = roleFilter ? allLinks.filter((link) => link.role === roleFilter) : allLinks if (claimLinks.length === 0) { throw new Error( allLinks.length === 0 ? `no_claim_links: ${desktopBootstrapPath} has no claim links (this workspace may have been bootstrapped without --prepare-desktop, or claimRoles was empty)` : `no_claim_links_for_role: no claim link with role "${roleFilter}" in ${desktopBootstrapPath}`, ) } jsonOut({ ok: true, message: "Claim link retrieved. Share this URL only with the person who should own this workspace.", bootstrapPath: desktopBootstrapPath, claimLinks, }, json) } async function runCloud(args) { const subcommand = args.positionals[1] if (subcommand === "claim-link") { runCloudClaimLink(args) return } if (subcommand === "onboard") { await runCloudOnboard(args) return } if (subcommand === "bootstrap-workspace") { await runCloudBootstrapWorkspace(args) return } printHelp() process.exitCode = 1 } async function main() { const args = parseArgs(process.argv.slice(2)) if (hasFlag(args.flags, "help") || args.positionals[0] === "help") { printHelp() return } if (hasFlag(args.flags, "version")) { console.log(VERSION) return } const command = args.positionals[0] || "help" if (command === "install") { runInstall(args) return } if (command === "doctor") { await runDoctor(args) return } if (command === "cloud") { await runCloud(args) return } printHelp() process.exitCode = 1 } main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)) process.exitCode = 1 })