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 +12 -0
- package/README.md +6 -2
- package/action/dist/index.js +28 -0
- package/dist/cli/index.js +267 -21
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/web/dist/assets/index-BJNqXAsU.js +10 -0
- package/src/web/dist/assets/{index-Bj8suDpq.css → index-Cmz0HGAE.css} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-DOLQwdCe.js +0 -10
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
|
package/action/dist/index.js
CHANGED
|
@@ -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/
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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",
|
|
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.
|
|
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(
|
|
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" ?
|
|
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 ${
|
|
3629
|
-
console.log(`Dashboard -> ${
|
|
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 -> ${
|
|
3886
|
+
Dashboard running at -> ${pc6.cyan(server.url)}`);
|
|
3644
3887
|
}
|
|
3645
3888
|
|
|
3646
3889
|
// src/cli/commands/serve.ts
|
|
3647
|
-
import
|
|
3890
|
+
import pc7 from "picocolors";
|
|
3648
3891
|
async function serveCommand(options) {
|
|
3649
|
-
console.log(
|
|
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(` ${
|
|
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 -> ${
|
|
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
|
|
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 ${
|
|
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(` ${
|
|
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 ${
|
|
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
|
});
|