devsurface 0.5.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ - Added guided onboarding with a setup readiness score (0–100%) computed from scan and doctor results.
6
+ - Added `devsurface onboard` CLI command that prints a colored checklist of setup steps with done/todo/manual status.
7
+ - Added `/api/onboarding` endpoint (and `/api/workspaces/:id/onboarding` for hub mode) returning the full onboarding plan.
8
+ - Added Onboarding tab in the dashboard with progress bar, per-step actions (install deps, copy `.env`, run scripts, open docs), and a compact banner on the overview when the project is not yet ready.
9
+ - Added `setupGuide` (or `setup_guide`) field in `devsurface.config.json` for maintainers to embed ordered setup instructions (max 24 steps, 200 chars each).
10
+ - Added Python, Go, and Java language support to the onboarding plan; install step is skipped for non-Node projects.
11
+ - Hardened hub workspace registry file permissions to `0o600`.
12
+ - Added a startup warning when `DEVSURFACE_WORKSPACE_ROOTS` is unset in container mode.
13
+ - Shifted keyboard shortcuts: 2=Onboarding, 3=Scripts, 8=Logs (previously 7).
14
+
3
15
  ## 0.5.0
4
16
 
5
17
  - Added framework presets for Next.js, Vite, Express, Fastify, NestJS, Remix, and Prisma.
package/README.md CHANGED
@@ -328,12 +328,16 @@ DevSurface is designed for local development.
328
328
 
329
329
  - Local dashboard servers bind to loopback hosts.
330
330
  - Container deployments use `DEVSURFACE_CONTAINER=true`.
331
- - Workspace registration can be limited with `DEVSURFACE_WORKSPACE_ROOTS`.
331
+ - Workspace registration can be limited with `DEVSURFACE_WORKSPACE_ROOTS`. In container
332
+ or shared-host deployments, set this to restrict which directories the hub will accept;
333
+ on a single-user laptop it is optional and DevSurface starts with no extra config.
332
334
  - `.env` values are never returned by scanners, API routes, CLI output, or UI panels.
333
335
  - Dashboard command runs show the exact command string first.
334
336
  - Docker service start and stop actions show the exact Compose command before running.
335
337
  - Destructive-looking configured commands, such as `rm -rf`, `docker volume rm`,
336
- database drops, and `git clean -fd`, are visibly marked before execution.
338
+ database drops, and `git clean -fd`, are visibly marked before execution. This list is a
339
+ helpful warning, not a sandbox: it flags common footguns for confirmation but does not
340
+ attempt to detect every dangerous command. Treat package scripts as code that runs as you.
337
341
  - Child processes started by DevSurface are cleaned up when the dashboard exits.
338
342
 
339
343
  ## FAQ
@@ -68,6 +68,33 @@ function toGroups(value, warnings) {
68
68
  }
69
69
  return groups;
70
70
  }
71
+ var MAX_SETUP_GUIDE_STEPS = 24;
72
+ var MAX_SETUP_GUIDE_STEP_LENGTH = 200;
73
+ function toSetupGuide(value, warnings) {
74
+ if (value === void 0) {
75
+ return void 0;
76
+ }
77
+ if (!Array.isArray(value)) {
78
+ warnings.push("setupGuide must be an array of strings.");
79
+ return void 0;
80
+ }
81
+ const steps = [];
82
+ for (const entry of value) {
83
+ if (typeof entry !== "string") {
84
+ warnings.push("setupGuide entries must be strings.");
85
+ continue;
86
+ }
87
+ const trimmed = entry.trim();
88
+ if (trimmed.length === 0) {
89
+ continue;
90
+ }
91
+ steps.push(trimmed.slice(0, MAX_SETUP_GUIDE_STEP_LENGTH));
92
+ }
93
+ if (steps.length > MAX_SETUP_GUIDE_STEPS) {
94
+ warnings.push(`setupGuide may contain at most ${MAX_SETUP_GUIDE_STEPS} steps.`);
95
+ }
96
+ return steps.slice(0, MAX_SETUP_GUIDE_STEPS);
97
+ }
71
98
  function toPorts(value, warnings) {
72
99
  if (value === void 0) {
73
100
  return void 0;
@@ -122,6 +149,7 @@ function validateConfig(raw) {
122
149
  ports: toPorts(raw.ports, warnings),
123
150
  env,
124
151
  services,
152
+ setupGuide: toSetupGuide(raw.setupGuide ?? raw.setup_guide, warnings),
125
153
  docs
126
154
  },
127
155
  warnings
package/dist/cli/index.js CHANGED
@@ -75,6 +75,12 @@ var defaultConfig = {
75
75
  services: {
76
76
  docker: true
77
77
  },
78
+ setupGuide: [
79
+ "Copy .env.example to .env",
80
+ "Fill in required environment values",
81
+ "Install dependencies",
82
+ "Start the dev server"
83
+ ],
78
84
  docs: ""
79
85
  };
80
86
 
@@ -123,6 +129,33 @@ function toGroups(value, warnings) {
123
129
  }
124
130
  return groups;
125
131
  }
132
+ var MAX_SETUP_GUIDE_STEPS = 24;
133
+ var MAX_SETUP_GUIDE_STEP_LENGTH = 200;
134
+ function toSetupGuide(value, warnings) {
135
+ if (value === void 0) {
136
+ return void 0;
137
+ }
138
+ if (!Array.isArray(value)) {
139
+ warnings.push("setupGuide must be an array of strings.");
140
+ return void 0;
141
+ }
142
+ const steps = [];
143
+ for (const entry of value) {
144
+ if (typeof entry !== "string") {
145
+ warnings.push("setupGuide entries must be strings.");
146
+ continue;
147
+ }
148
+ const trimmed = entry.trim();
149
+ if (trimmed.length === 0) {
150
+ continue;
151
+ }
152
+ steps.push(trimmed.slice(0, MAX_SETUP_GUIDE_STEP_LENGTH));
153
+ }
154
+ if (steps.length > MAX_SETUP_GUIDE_STEPS) {
155
+ warnings.push(`setupGuide may contain at most ${MAX_SETUP_GUIDE_STEPS} steps.`);
156
+ }
157
+ return steps.slice(0, MAX_SETUP_GUIDE_STEPS);
158
+ }
126
159
  function toPorts(value, warnings) {
127
160
  if (value === void 0) {
128
161
  return void 0;
@@ -177,6 +210,7 @@ function validateConfig(raw) {
177
210
  ports: toPorts(raw.ports, warnings),
178
211
  env,
179
212
  services,
213
+ setupGuide: toSetupGuide(raw.setupGuide ?? raw.setup_guide, warnings),
180
214
  docs
181
215
  },
182
216
  warnings
@@ -1549,9 +1583,188 @@ async function initCommand(cwd = process.cwd()) {
1549
1583
  }
1550
1584
  }
1551
1585
 
1552
- // src/cli/commands/run.ts
1586
+ // src/cli/commands/onboard.ts
1553
1587
  import pc3 from "picocolors";
1554
1588
 
1589
+ // src/core/onboarding/index.ts
1590
+ function hasWarning(warnings, id) {
1591
+ return warnings.some((warning2) => warning2.id === id);
1592
+ }
1593
+ function pickStartAction(scan) {
1594
+ if (scan.scripts.dev !== void 0) {
1595
+ return { kind: "run-script", label: "Start dev server", target: "dev" };
1596
+ }
1597
+ if (scan.scripts.start !== void 0) {
1598
+ return { kind: "run-script", label: "Start app", target: "start" };
1599
+ }
1600
+ const configuredCommands = {
1601
+ ...scan.presetCommands,
1602
+ ...scan.config?.config.commands
1603
+ };
1604
+ for (const name of ["dev", "start", "serve"]) {
1605
+ if (configuredCommands[name] !== void 0) {
1606
+ return { kind: "run-command", label: `Run ${name}`, target: name };
1607
+ }
1608
+ }
1609
+ return null;
1610
+ }
1611
+ function dockerStep(scan) {
1612
+ const docker = scan.docker;
1613
+ if (docker === null) {
1614
+ return null;
1615
+ }
1616
+ const runningServices = docker.services.filter((service) => service.status === "running");
1617
+ const allRunning = docker.services.length > 0 && runningServices.length === docker.services.length;
1618
+ if (docker.daemonStatus !== "running") {
1619
+ return {
1620
+ id: "docker-start",
1621
+ title: "Start Docker services",
1622
+ description: docker.message ?? "A Docker Compose file was found, but the Docker engine is not running.",
1623
+ status: "manual",
1624
+ blocking: false,
1625
+ action: { kind: "docker", label: "Open Services" }
1626
+ };
1627
+ }
1628
+ if (docker.services.length === 0 || allRunning) {
1629
+ return {
1630
+ id: "docker-start",
1631
+ title: "Start Docker services",
1632
+ description: docker.services.length === 0 ? "Docker is running. No Compose services need to be started." : "All Docker Compose services are running.",
1633
+ status: "done",
1634
+ blocking: false
1635
+ };
1636
+ }
1637
+ return {
1638
+ id: "docker-start",
1639
+ title: "Start Docker services",
1640
+ description: `${runningServices.length}/${docker.services.length} Compose services running. Start the rest in Services.`,
1641
+ status: "todo",
1642
+ blocking: false,
1643
+ action: { kind: "docker", label: "Open Services" }
1644
+ };
1645
+ }
1646
+ function buildOnboardingPlan(scan, warnings) {
1647
+ const steps = [];
1648
+ const isNodeProject = scan.language.detected.includes("node");
1649
+ if (isNodeProject && scan.packageJson !== null) {
1650
+ const needsInstall = hasWarning(warnings, "missing-node-modules");
1651
+ steps.push({
1652
+ id: "install-dependencies",
1653
+ title: "Install dependencies",
1654
+ description: needsInstall ? "node_modules is missing. Install dependencies before running scripts." : "Dependencies are installed.",
1655
+ status: needsInstall ? "todo" : "done",
1656
+ blocking: true,
1657
+ action: needsInstall ? { kind: "install", label: "Install" } : void 0
1658
+ });
1659
+ }
1660
+ if (scan.env?.hasExample) {
1661
+ const missingLocal = !scan.env.hasLocal;
1662
+ steps.push({
1663
+ id: "create-env",
1664
+ title: "Create .env file",
1665
+ description: missingLocal ? ".env.example exists but the local .env file is missing." : ".env is present.",
1666
+ status: missingLocal ? "todo" : "done",
1667
+ blocking: true,
1668
+ action: missingLocal ? { kind: "env-copy", label: "Copy .env" } : void 0
1669
+ });
1670
+ if (scan.env.hasLocal) {
1671
+ const unset = [.../* @__PURE__ */ new Set([...scan.env.missingKeys, ...scan.env.emptyKeys])];
1672
+ steps.push({
1673
+ id: "fill-env",
1674
+ title: "Fill in environment values",
1675
+ description: unset.length > 0 ? `Set values for: ${unset.join(", ")}. Values are intentionally hidden.` : "All environment keys from the example are present.",
1676
+ status: unset.length > 0 ? "manual" : "done",
1677
+ blocking: true
1678
+ });
1679
+ }
1680
+ }
1681
+ const docker = dockerStep(scan);
1682
+ if (docker !== null) {
1683
+ steps.push(docker);
1684
+ }
1685
+ const portsInUse = scan.ports.filter((probe) => probe.inUse);
1686
+ if (portsInUse.length > 0) {
1687
+ steps.push({
1688
+ id: "free-ports",
1689
+ title: "Resolve port conflicts",
1690
+ description: `Already in use: ${portsInUse.map((probe) => probe.port).join(", ")}. Stop the conflicting process or change the port.`,
1691
+ status: "manual",
1692
+ blocking: false
1693
+ });
1694
+ }
1695
+ for (const [index, entry] of (scan.config?.config.setupGuide ?? []).entries()) {
1696
+ steps.push({
1697
+ id: `guide-${index}`,
1698
+ title: entry,
1699
+ description: "From the project setup guide.",
1700
+ status: "manual",
1701
+ blocking: false
1702
+ });
1703
+ }
1704
+ const docs = scan.config?.config.docs;
1705
+ if (typeof docs === "string" && docs.length > 0 && isSafeHttpUrl(docs)) {
1706
+ steps.push({
1707
+ id: "read-docs",
1708
+ title: "Read the project docs",
1709
+ description: docs,
1710
+ status: "manual",
1711
+ blocking: false,
1712
+ action: { kind: "open-docs", label: "Open docs", target: docs }
1713
+ });
1714
+ }
1715
+ const startAction = pickStartAction(scan);
1716
+ if (startAction !== null) {
1717
+ steps.push({
1718
+ id: "start-app",
1719
+ title: "Start the app",
1720
+ description: "Run the development server once setup is complete.",
1721
+ status: "todo",
1722
+ blocking: false,
1723
+ action: startAction
1724
+ });
1725
+ }
1726
+ const blocking = steps.filter((step) => step.blocking);
1727
+ const blockingDone = blocking.filter((step) => step.status === "done");
1728
+ const readiness = blocking.length === 0 ? 100 : Math.round(blockingDone.length / blocking.length * 100);
1729
+ const ready = readiness === 100;
1730
+ const remaining = blocking.length - blockingDone.length;
1731
+ const summary = ready ? "Project is ready to run." : `${remaining} setup step${remaining === 1 ? "" : "s"} remaining before the project is ready.`;
1732
+ return { steps, readiness, ready, summary };
1733
+ }
1734
+
1735
+ // src/cli/commands/onboard.ts
1736
+ function statusGlyph(status) {
1737
+ if (status === "done") {
1738
+ return pc3.green("[x]");
1739
+ }
1740
+ if (status === "todo") {
1741
+ return pc3.yellow("[ ]");
1742
+ }
1743
+ return pc3.cyan("[~]");
1744
+ }
1745
+ async function onboardCommand(cwd = process.cwd()) {
1746
+ const scan = await scanProject(cwd);
1747
+ const warnings = await runDoctor(cwd, scan);
1748
+ const plan = buildOnboardingPlan(scan, warnings);
1749
+ console.log(pc3.bold(`Onboarding ${safeDisplayText(scan.projectName)}`));
1750
+ console.log(`${plan.readiness}% ready \u2014 ${safeDisplayText(plan.summary)}`);
1751
+ console.log("");
1752
+ if (plan.steps.length === 0) {
1753
+ console.log(pc3.green("No onboarding steps detected."));
1754
+ return;
1755
+ }
1756
+ for (const step of plan.steps) {
1757
+ console.log(`${statusGlyph(step.status)} ${pc3.bold(safeDisplayText(step.title))}`);
1758
+ console.log(` ${safeDisplayText(step.description)}`);
1759
+ if (step.action && step.status !== "done") {
1760
+ console.log(pc3.dim(` \u2192 ${safeDisplayText(step.action.label)}`));
1761
+ }
1762
+ }
1763
+ }
1764
+
1765
+ // src/cli/commands/run.ts
1766
+ import pc4 from "picocolors";
1767
+
1555
1768
  // src/core/process/runner.ts
1556
1769
  import spawn2 from "cross-spawn";
1557
1770
 
@@ -1765,7 +1978,7 @@ async function runCommand(script, cwd = process.cwd()) {
1765
1978
  const configuredCommand = scan.config?.config.commands?.[script] ?? scan.presetCommands[script];
1766
1979
  if (configuredCommand !== void 0) {
1767
1980
  if (isDangerousCommand(configuredCommand)) {
1768
- console.error(pc3.red(`Refusing to run dangerous command "${safeDisplayText(script)}".`));
1981
+ console.error(pc4.red(`Refusing to run dangerous command "${safeDisplayText(script)}".`));
1769
1982
  process.exitCode = 1;
1770
1983
  return;
1771
1984
  }
@@ -1782,17 +1995,17 @@ async function runCommand(script, cwd = process.cwd()) {
1782
1995
  ...Object.keys(scan.presetCommands)
1783
1996
  ];
1784
1997
  const hint = available.length > 0 ? ` Available commands: ${safeDisplayList(available)}.` : "";
1785
- console.error(pc3.red(`Command "${safeDisplayText(script)}" was not found.${hint}`));
1998
+ console.error(pc4.red(`Command "${safeDisplayText(script)}" was not found.${hint}`));
1786
1999
  process.exitCode = 1;
1787
2000
  }
1788
2001
 
1789
2002
  // src/cli/commands/scan.ts
1790
- import pc4 from "picocolors";
2003
+ import pc5 from "picocolors";
1791
2004
  function formatList(values) {
1792
2005
  return safeDisplayList(values);
1793
2006
  }
1794
2007
  function printScanResult(scan) {
1795
- console.log(pc4.bold(`Project: ${safeDisplayText(scan.projectName)}`));
2008
+ console.log(pc5.bold(`Project: ${safeDisplayText(scan.projectName)}`));
1796
2009
  console.log(`Language: ${formatList(scan.language.detected) || "unknown"}`);
1797
2010
  console.log(`Type: ${safeDisplayText(scan.framework?.type ?? "Unknown")}`);
1798
2011
  console.log(`Manager: ${safeDisplayText(scan.packageManager ?? "unknown")}`);
@@ -1819,7 +2032,7 @@ async function scanCommand(cwd = process.cwd()) {
1819
2032
  }
1820
2033
 
1821
2034
  // src/cli/commands/start.ts
1822
- import pc5 from "picocolors";
2035
+ import pc6 from "picocolors";
1823
2036
 
1824
2037
  // node_modules/open/index.js
1825
2038
  import process7 from "process";
@@ -2634,7 +2847,10 @@ var WorkspaceRegistry = class {
2634
2847
  }
2635
2848
  async write(entries) {
2636
2849
  await fs19.mkdir(path15.dirname(this.filePath), { recursive: true });
2637
- await fs19.writeFile(this.filePath, JSON.stringify(entries, null, 2) + "\n", "utf8");
2850
+ await fs19.writeFile(this.filePath, JSON.stringify(entries, null, 2) + "\n", {
2851
+ encoding: "utf8",
2852
+ mode: 384
2853
+ });
2638
2854
  }
2639
2855
  async seedFromEnv() {
2640
2856
  if (this.seeded) {
@@ -2721,7 +2937,7 @@ import path16 from "path";
2721
2937
  import spawn4 from "cross-spawn";
2722
2938
 
2723
2939
  // src/version.ts
2724
- var DEV_SURFACE_VERSION = "0.5.0";
2940
+ var DEV_SURFACE_VERSION = "0.6.0";
2725
2941
 
2726
2942
  // src/server/localAccess.ts
2727
2943
  var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
@@ -3082,6 +3298,11 @@ function handleDockerError(error, context) {
3082
3298
  }
3083
3299
  throw error;
3084
3300
  }
3301
+ async function onboardingForRoot(root) {
3302
+ const scan = await scanProject(root);
3303
+ const warnings = await runDoctor(root, scan);
3304
+ return buildOnboardingPlan(scan, warnings);
3305
+ }
3085
3306
  function registerWorkspaceRoutes(app, resolveWorkspace) {
3086
3307
  app.get("/api/workspaces/:id/project", async (context) => {
3087
3308
  const ws = await resolveWorkspace(context.req.param("id"));
@@ -3093,6 +3314,11 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
3093
3314
  if (!ws) return context.json({ error: "Workspace not found." }, 404);
3094
3315
  return context.json(await runDoctor(ws.root));
3095
3316
  });
3317
+ app.get("/api/workspaces/:id/onboarding", async (context) => {
3318
+ const ws = await resolveWorkspace(context.req.param("id"));
3319
+ if (!ws) return context.json({ error: "Workspace not found." }, 404);
3320
+ return context.json(await onboardingForRoot(ws.root));
3321
+ });
3096
3322
  app.get("/api/workspaces/:id/processes", async (context) => {
3097
3323
  const ws = await resolveWorkspace(context.req.param("id"));
3098
3324
  if (!ws) return context.json({ error: "Workspace not found." }, 404);
@@ -3315,6 +3541,11 @@ function registerHubApiRoutes(app, options) {
3315
3541
  if (entries.length === 0) return context.json({ error: "No workspaces registered." }, 404);
3316
3542
  return context.json(await runDoctor(hub.ensure(entries[0]).root));
3317
3543
  });
3544
+ app.get("/api/onboarding", async (context) => {
3545
+ const entries = await hub.registry.list();
3546
+ if (entries.length === 0) return context.json({ error: "No workspaces registered." }, 404);
3547
+ return context.json(await onboardingForRoot(hub.ensure(entries[0]).root));
3548
+ });
3318
3549
  app.get("/api/processes", async (context) => {
3319
3550
  const entries = await hub.registry.list();
3320
3551
  if (entries.length === 0) return context.json([]);
@@ -3418,6 +3649,17 @@ function setupHubWebSocket(server, hub) {
3418
3649
  }
3419
3650
 
3420
3651
  // src/server/index.ts
3652
+ function warnIfContainerRootsUnset(host) {
3653
+ if (host !== "0.0.0.0" && host !== "::") {
3654
+ return;
3655
+ }
3656
+ if (process.env.DEVSURFACE_WORKSPACE_ROOTS?.trim()) {
3657
+ return;
3658
+ }
3659
+ console.warn(
3660
+ "Warning: DEVSURFACE_WORKSPACE_ROOTS is unset in container mode. Any loopback or private-network client can register arbitrary directories as workspaces. Set DEVSURFACE_WORKSPACE_ROOTS to restrict workspace registration."
3661
+ );
3662
+ }
3421
3663
  async function fileExists(filePath) {
3422
3664
  try {
3423
3665
  await fs21.access(filePath);
@@ -3533,6 +3775,7 @@ async function startHubServer(options) {
3533
3775
  const port = options.port ?? DEFAULT_PORT;
3534
3776
  const hub = new Hub({ dataDir: options.dataDir });
3535
3777
  hub.attachCleanupHandlers();
3778
+ warnIfContainerRootsUnset(host);
3536
3779
  if (options.initialWorkspace) {
3537
3780
  await hub.registry.add(options.initialWorkspace);
3538
3781
  }
@@ -3608,7 +3851,7 @@ function dashboardUrl(workspaceId2, port = DEFAULT_PORT, host = DEFAULT_HOST) {
3608
3851
  async function startCommand(options) {
3609
3852
  const cwd = options.cwd ?? process.cwd();
3610
3853
  const port = options.port ?? 4567;
3611
- console.log(pc5.bold(`DevSurface v${DEV_SURFACE_VERSION}`));
3854
+ console.log(pc6.bold(`DevSurface v${DEV_SURFACE_VERSION}`));
3612
3855
  console.log("Scanning project...\n");
3613
3856
  const scan = await scanProject(cwd);
3614
3857
  printScanResult(scan);
@@ -3616,7 +3859,7 @@ async function startCommand(options) {
3616
3859
  if (warnings.length > 0) {
3617
3860
  console.log("\nWarnings:");
3618
3861
  for (const item of warnings) {
3619
- const marker = item.severity === "error" ? pc5.red("!") : pc5.yellow("!");
3862
+ const marker = item.severity === "error" ? pc6.red("!") : pc6.yellow("!");
3620
3863
  console.log(` ${marker} ${item.title}`);
3621
3864
  }
3622
3865
  }
@@ -3625,8 +3868,8 @@ async function startCommand(options) {
3625
3868
  const registered = await registerWorkspaceRemotely(cwd, port);
3626
3869
  if (registered) {
3627
3870
  const url = dashboardUrl(registered.id, port);
3628
- console.log(`Workspace ${pc5.cyan(registered.name)} attached.`);
3629
- console.log(`Dashboard -> ${pc5.cyan(url)}`);
3871
+ console.log(`Workspace ${pc6.cyan(registered.name)} attached.`);
3872
+ console.log(`Dashboard -> ${pc6.cyan(url)}`);
3630
3873
  if (options.openBrowser !== false) {
3631
3874
  await open_default(url);
3632
3875
  }
@@ -3640,13 +3883,13 @@ async function startCommand(options) {
3640
3883
  initialWorkspace: cwd
3641
3884
  });
3642
3885
  console.log(`
3643
- Dashboard running at -> ${pc5.cyan(server.url)}`);
3886
+ Dashboard running at -> ${pc6.cyan(server.url)}`);
3644
3887
  }
3645
3888
 
3646
3889
  // src/cli/commands/serve.ts
3647
- import pc6 from "picocolors";
3890
+ import pc7 from "picocolors";
3648
3891
  async function serveCommand(options) {
3649
- console.log(pc6.bold(`DevSurface Hub v${DEV_SURFACE_VERSION}`));
3892
+ console.log(pc7.bold(`DevSurface Hub v${DEV_SURFACE_VERSION}`));
3650
3893
  console.log("Starting hub server...\n");
3651
3894
  const server = await startHubServer({
3652
3895
  port: options.port,
@@ -3656,7 +3899,7 @@ async function serveCommand(options) {
3656
3899
  if (summaries.length > 0) {
3657
3900
  console.log(`Registered workspaces: ${summaries.length}`);
3658
3901
  for (const ws of summaries) {
3659
- console.log(` ${pc6.cyan(ws.name)} -> ${ws.path}`);
3902
+ console.log(` ${pc7.cyan(ws.name)} -> ${ws.path}`);
3660
3903
  }
3661
3904
  } else {
3662
3905
  console.log(
@@ -3664,17 +3907,17 @@ async function serveCommand(options) {
3664
3907
  );
3665
3908
  }
3666
3909
  console.log(`
3667
- Hub running at -> ${pc6.cyan(server.url)}`);
3910
+ Hub running at -> ${pc7.cyan(server.url)}`);
3668
3911
  }
3669
3912
 
3670
3913
  // src/cli/commands/workspace.ts
3671
3914
  import path18 from "path";
3672
- import pc7 from "picocolors";
3915
+ import pc8 from "picocolors";
3673
3916
  async function workspaceAddCommand(dirPath) {
3674
3917
  const registry = new WorkspaceRegistry();
3675
3918
  const target = path18.resolve(dirPath ?? process.cwd());
3676
3919
  const entry = await registry.add(target);
3677
- console.log(`Added workspace ${pc7.cyan(entry.name)} (${entry.id}) -> ${entry.path}`);
3920
+ console.log(`Added workspace ${pc8.cyan(entry.name)} (${entry.id}) -> ${entry.path}`);
3678
3921
  }
3679
3922
  async function workspaceListCommand() {
3680
3923
  const registry = new WorkspaceRegistry();
@@ -3688,7 +3931,7 @@ async function workspaceListCommand() {
3688
3931
  console.log(`${entries.length} workspace${entries.length === 1 ? "" : "s"}:
3689
3932
  `);
3690
3933
  for (const entry of entries) {
3691
- console.log(` ${pc7.cyan(entry.name)} (${entry.id})`);
3934
+ console.log(` ${pc8.cyan(entry.name)} (${entry.id})`);
3692
3935
  console.log(` ${entry.path}`);
3693
3936
  }
3694
3937
  }
@@ -3696,7 +3939,7 @@ async function workspaceRemoveCommand(id) {
3696
3939
  const registry = new WorkspaceRegistry();
3697
3940
  const removed = await registry.remove(id);
3698
3941
  if (removed) {
3699
- console.log(`Removed workspace ${pc7.cyan(id)}.`);
3942
+ console.log(`Removed workspace ${pc8.cyan(id)}.`);
3700
3943
  } else {
3701
3944
  console.error(`Workspace "${id}" not found.`);
3702
3945
  process.exitCode = 1;
@@ -3826,6 +4069,9 @@ program.command("scan").description("Print detected project info.").action(() =>
3826
4069
  program.command("doctor").description("Print setup health warnings.").action(() => {
3827
4070
  handle(doctorCommand(process.cwd()));
3828
4071
  });
4072
+ program.command("onboard").description("Print a guided setup checklist with readiness score.").action(() => {
4073
+ handle(onboardCommand(process.cwd()));
4074
+ });
3829
4075
  program.command("init").description("Create a starter devsurface.config.json.").action(() => {
3830
4076
  handle(initCommand(process.cwd()));
3831
4077
  });