devsurface 0.5.0 → 0.7.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 +18 -0
- package/README.md +6 -2
- package/action/dist/index.js +45 -0
- package/dist/cli/index.js +321 -24
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/web/dist/assets/index-CrNWwfPe.css +1 -0
- package/src/web/dist/assets/index-DIB90iA-.js +10 -0
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-Bj8suDpq.css +0 -1
- package/src/web/dist/assets/index-DOLQwdCe.js +0 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
- Added structured `setupGuide` steps in `devsurface.config.json`: each step can be a plain string or an object with `title`, `description`, and a `command` or `script` key that turns it into a one-click action button in the Onboarding tab.
|
|
6
|
+
- Updated `devsurface init` to generate a richer config with grouped commands (First-time setup, Daily development, Before committing) and actionable setup steps.
|
|
7
|
+
- Added plain-English explanations for every package script in the dashboard, so non-technical users can see what a command like `vite` or `tsc --noEmit` actually does before running it.
|
|
8
|
+
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
- Added guided onboarding with a setup readiness score (0–100%) computed from scan and doctor results.
|
|
12
|
+
- Added `devsurface onboard` CLI command that prints a colored checklist of setup steps with done/todo/manual status.
|
|
13
|
+
- Added `/api/onboarding` endpoint (and `/api/workspaces/:id/onboarding` for hub mode) returning the full onboarding plan.
|
|
14
|
+
- 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.
|
|
15
|
+
- Added `setupGuide` (or `setup_guide`) field in `devsurface.config.json` for maintainers to embed ordered setup instructions (max 24 steps, 200 chars each).
|
|
16
|
+
- Added Python, Go, and Java language support to the onboarding plan; install step is skipped for non-Node projects.
|
|
17
|
+
- Hardened hub workspace registry file permissions to `0o600`.
|
|
18
|
+
- Added a startup warning when `DEVSURFACE_WORKSPACE_ROOTS` is unset in container mode.
|
|
19
|
+
- Shifted keyboard shortcuts: 2=Onboarding, 3=Scripts, 8=Logs (previously 7).
|
|
20
|
+
|
|
3
21
|
## 0.5.0
|
|
4
22
|
|
|
5
23
|
- 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,50 @@ 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 or step objects.");
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
const steps = [];
|
|
82
|
+
for (const entry of value) {
|
|
83
|
+
if (typeof entry === "string") {
|
|
84
|
+
const trimmed = entry.trim();
|
|
85
|
+
if (trimmed.length > 0) {
|
|
86
|
+
steps.push(trimmed.slice(0, MAX_SETUP_GUIDE_STEP_LENGTH));
|
|
87
|
+
}
|
|
88
|
+
} else if (isRecord(entry)) {
|
|
89
|
+
if (typeof entry.title !== "string" || entry.title.trim().length === 0) {
|
|
90
|
+
warnings.push("setupGuide step objects must have a non-empty title string.");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const step = {
|
|
94
|
+
title: entry.title.trim().slice(0, MAX_SETUP_GUIDE_STEP_LENGTH)
|
|
95
|
+
};
|
|
96
|
+
if (typeof entry.description === "string" && entry.description.trim().length > 0) {
|
|
97
|
+
step.description = entry.description.trim().slice(0, MAX_SETUP_GUIDE_STEP_LENGTH);
|
|
98
|
+
}
|
|
99
|
+
if (typeof entry.command === "string" && entry.command.trim().length > 0) {
|
|
100
|
+
step.command = entry.command.trim();
|
|
101
|
+
}
|
|
102
|
+
if (typeof entry.script === "string" && entry.script.trim().length > 0) {
|
|
103
|
+
step.script = entry.script.trim();
|
|
104
|
+
}
|
|
105
|
+
steps.push(step);
|
|
106
|
+
} else {
|
|
107
|
+
warnings.push("setupGuide entries must be strings or step objects.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (steps.length > MAX_SETUP_GUIDE_STEPS) {
|
|
111
|
+
warnings.push(`setupGuide may contain at most ${MAX_SETUP_GUIDE_STEPS} steps.`);
|
|
112
|
+
}
|
|
113
|
+
return steps.slice(0, MAX_SETUP_GUIDE_STEPS);
|
|
114
|
+
}
|
|
71
115
|
function toPorts(value, warnings) {
|
|
72
116
|
if (value === void 0) {
|
|
73
117
|
return void 0;
|
|
@@ -122,6 +166,7 @@ function validateConfig(raw) {
|
|
|
122
166
|
ports: toPorts(raw.ports, warnings),
|
|
123
167
|
env,
|
|
124
168
|
services,
|
|
169
|
+
setupGuide: toSetupGuide(raw.setupGuide ?? raw.setup_guide, warnings),
|
|
125
170
|
docs
|
|
126
171
|
},
|
|
127
172
|
warnings
|
package/dist/cli/index.js
CHANGED
|
@@ -56,15 +56,17 @@ var defaultConfig = {
|
|
|
56
56
|
description: "Local developer control panel",
|
|
57
57
|
commands: {
|
|
58
58
|
install: "npm install",
|
|
59
|
+
migrate: "npm run db:migrate",
|
|
60
|
+
seed: "npm run db:seed",
|
|
59
61
|
dev: "npm run dev",
|
|
60
62
|
build: "npm run build",
|
|
61
63
|
test: "npm test",
|
|
62
64
|
lint: "npm run lint"
|
|
63
65
|
},
|
|
64
66
|
groups: {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
"First-time setup": ["install", "migrate", "seed"],
|
|
68
|
+
"Daily development": ["dev"],
|
|
69
|
+
"Before committing": ["test", "lint"],
|
|
68
70
|
Build: ["build"]
|
|
69
71
|
},
|
|
70
72
|
ports: [3e3],
|
|
@@ -75,6 +77,25 @@ var defaultConfig = {
|
|
|
75
77
|
services: {
|
|
76
78
|
docker: true
|
|
77
79
|
},
|
|
80
|
+
setupGuide: [
|
|
81
|
+
{
|
|
82
|
+
title: "Install dependencies",
|
|
83
|
+
description: "Run the package manager install to set up node_modules.",
|
|
84
|
+
command: "install"
|
|
85
|
+
},
|
|
86
|
+
"Copy .env.example to .env",
|
|
87
|
+
"Fill in required environment values (DATABASE_URL, etc.)",
|
|
88
|
+
{
|
|
89
|
+
title: "Run database migrations",
|
|
90
|
+
description: "Apply the database schema to your local database.",
|
|
91
|
+
command: "migrate"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
title: "Start the development server",
|
|
95
|
+
description: "Run the local dev server and open the app in a browser.",
|
|
96
|
+
command: "dev"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
78
99
|
docs: ""
|
|
79
100
|
};
|
|
80
101
|
|
|
@@ -123,6 +144,50 @@ function toGroups(value, warnings) {
|
|
|
123
144
|
}
|
|
124
145
|
return groups;
|
|
125
146
|
}
|
|
147
|
+
var MAX_SETUP_GUIDE_STEPS = 24;
|
|
148
|
+
var MAX_SETUP_GUIDE_STEP_LENGTH = 200;
|
|
149
|
+
function toSetupGuide(value, warnings) {
|
|
150
|
+
if (value === void 0) {
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(value)) {
|
|
154
|
+
warnings.push("setupGuide must be an array of strings or step objects.");
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
const steps = [];
|
|
158
|
+
for (const entry of value) {
|
|
159
|
+
if (typeof entry === "string") {
|
|
160
|
+
const trimmed = entry.trim();
|
|
161
|
+
if (trimmed.length > 0) {
|
|
162
|
+
steps.push(trimmed.slice(0, MAX_SETUP_GUIDE_STEP_LENGTH));
|
|
163
|
+
}
|
|
164
|
+
} else if (isRecord(entry)) {
|
|
165
|
+
if (typeof entry.title !== "string" || entry.title.trim().length === 0) {
|
|
166
|
+
warnings.push("setupGuide step objects must have a non-empty title string.");
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const step = {
|
|
170
|
+
title: entry.title.trim().slice(0, MAX_SETUP_GUIDE_STEP_LENGTH)
|
|
171
|
+
};
|
|
172
|
+
if (typeof entry.description === "string" && entry.description.trim().length > 0) {
|
|
173
|
+
step.description = entry.description.trim().slice(0, MAX_SETUP_GUIDE_STEP_LENGTH);
|
|
174
|
+
}
|
|
175
|
+
if (typeof entry.command === "string" && entry.command.trim().length > 0) {
|
|
176
|
+
step.command = entry.command.trim();
|
|
177
|
+
}
|
|
178
|
+
if (typeof entry.script === "string" && entry.script.trim().length > 0) {
|
|
179
|
+
step.script = entry.script.trim();
|
|
180
|
+
}
|
|
181
|
+
steps.push(step);
|
|
182
|
+
} else {
|
|
183
|
+
warnings.push("setupGuide entries must be strings or step objects.");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (steps.length > MAX_SETUP_GUIDE_STEPS) {
|
|
187
|
+
warnings.push(`setupGuide may contain at most ${MAX_SETUP_GUIDE_STEPS} steps.`);
|
|
188
|
+
}
|
|
189
|
+
return steps.slice(0, MAX_SETUP_GUIDE_STEPS);
|
|
190
|
+
}
|
|
126
191
|
function toPorts(value, warnings) {
|
|
127
192
|
if (value === void 0) {
|
|
128
193
|
return void 0;
|
|
@@ -177,6 +242,7 @@ function validateConfig(raw) {
|
|
|
177
242
|
ports: toPorts(raw.ports, warnings),
|
|
178
243
|
env,
|
|
179
244
|
services,
|
|
245
|
+
setupGuide: toSetupGuide(raw.setupGuide ?? raw.setup_guide, warnings),
|
|
180
246
|
docs
|
|
181
247
|
},
|
|
182
248
|
warnings
|
|
@@ -1549,9 +1615,207 @@ async function initCommand(cwd = process.cwd()) {
|
|
|
1549
1615
|
}
|
|
1550
1616
|
}
|
|
1551
1617
|
|
|
1552
|
-
// src/cli/commands/
|
|
1618
|
+
// src/cli/commands/onboard.ts
|
|
1553
1619
|
import pc3 from "picocolors";
|
|
1554
1620
|
|
|
1621
|
+
// src/core/onboarding/index.ts
|
|
1622
|
+
function hasWarning(warnings, id) {
|
|
1623
|
+
return warnings.some((warning2) => warning2.id === id);
|
|
1624
|
+
}
|
|
1625
|
+
function pickStartAction(scan) {
|
|
1626
|
+
if (scan.scripts.dev !== void 0) {
|
|
1627
|
+
return { kind: "run-script", label: "Start dev server", target: "dev" };
|
|
1628
|
+
}
|
|
1629
|
+
if (scan.scripts.start !== void 0) {
|
|
1630
|
+
return { kind: "run-script", label: "Start app", target: "start" };
|
|
1631
|
+
}
|
|
1632
|
+
const configuredCommands = {
|
|
1633
|
+
...scan.presetCommands,
|
|
1634
|
+
...scan.config?.config.commands
|
|
1635
|
+
};
|
|
1636
|
+
for (const name of ["dev", "start", "serve"]) {
|
|
1637
|
+
if (configuredCommands[name] !== void 0) {
|
|
1638
|
+
return { kind: "run-command", label: `Run ${name}`, target: name };
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
function dockerStep(scan) {
|
|
1644
|
+
const docker = scan.docker;
|
|
1645
|
+
if (docker === null) {
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
const runningServices = docker.services.filter((service) => service.status === "running");
|
|
1649
|
+
const allRunning = docker.services.length > 0 && runningServices.length === docker.services.length;
|
|
1650
|
+
if (docker.daemonStatus !== "running") {
|
|
1651
|
+
return {
|
|
1652
|
+
id: "docker-start",
|
|
1653
|
+
title: "Start Docker services",
|
|
1654
|
+
description: docker.message ?? "A Docker Compose file was found, but the Docker engine is not running.",
|
|
1655
|
+
status: "manual",
|
|
1656
|
+
blocking: false,
|
|
1657
|
+
action: { kind: "docker", label: "Open Services" }
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
if (docker.services.length === 0 || allRunning) {
|
|
1661
|
+
return {
|
|
1662
|
+
id: "docker-start",
|
|
1663
|
+
title: "Start Docker services",
|
|
1664
|
+
description: docker.services.length === 0 ? "Docker is running. No Compose services need to be started." : "All Docker Compose services are running.",
|
|
1665
|
+
status: "done",
|
|
1666
|
+
blocking: false
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
return {
|
|
1670
|
+
id: "docker-start",
|
|
1671
|
+
title: "Start Docker services",
|
|
1672
|
+
description: `${runningServices.length}/${docker.services.length} Compose services running. Start the rest in Services.`,
|
|
1673
|
+
status: "todo",
|
|
1674
|
+
blocking: false,
|
|
1675
|
+
action: { kind: "docker", label: "Open Services" }
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function buildOnboardingPlan(scan, warnings) {
|
|
1679
|
+
const steps = [];
|
|
1680
|
+
const isNodeProject = scan.language.detected.includes("node");
|
|
1681
|
+
if (isNodeProject && scan.packageJson !== null) {
|
|
1682
|
+
const needsInstall = hasWarning(warnings, "missing-node-modules");
|
|
1683
|
+
steps.push({
|
|
1684
|
+
id: "install-dependencies",
|
|
1685
|
+
title: "Install dependencies",
|
|
1686
|
+
description: needsInstall ? "node_modules is missing. Install dependencies before running scripts." : "Dependencies are installed.",
|
|
1687
|
+
status: needsInstall ? "todo" : "done",
|
|
1688
|
+
blocking: true,
|
|
1689
|
+
action: needsInstall ? { kind: "install", label: "Install" } : void 0
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
if (scan.env?.hasExample) {
|
|
1693
|
+
const missingLocal = !scan.env.hasLocal;
|
|
1694
|
+
steps.push({
|
|
1695
|
+
id: "create-env",
|
|
1696
|
+
title: "Create .env file",
|
|
1697
|
+
description: missingLocal ? ".env.example exists but the local .env file is missing." : ".env is present.",
|
|
1698
|
+
status: missingLocal ? "todo" : "done",
|
|
1699
|
+
blocking: true,
|
|
1700
|
+
action: missingLocal ? { kind: "env-copy", label: "Copy .env" } : void 0
|
|
1701
|
+
});
|
|
1702
|
+
if (scan.env.hasLocal) {
|
|
1703
|
+
const unset = [.../* @__PURE__ */ new Set([...scan.env.missingKeys, ...scan.env.emptyKeys])];
|
|
1704
|
+
steps.push({
|
|
1705
|
+
id: "fill-env",
|
|
1706
|
+
title: "Fill in environment values",
|
|
1707
|
+
description: unset.length > 0 ? `Set values for: ${unset.join(", ")}. Values are intentionally hidden.` : "All environment keys from the example are present.",
|
|
1708
|
+
status: unset.length > 0 ? "manual" : "done",
|
|
1709
|
+
blocking: true
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const docker = dockerStep(scan);
|
|
1714
|
+
if (docker !== null) {
|
|
1715
|
+
steps.push(docker);
|
|
1716
|
+
}
|
|
1717
|
+
const portsInUse = scan.ports.filter((probe) => probe.inUse);
|
|
1718
|
+
if (portsInUse.length > 0) {
|
|
1719
|
+
steps.push({
|
|
1720
|
+
id: "free-ports",
|
|
1721
|
+
title: "Resolve port conflicts",
|
|
1722
|
+
description: `Already in use: ${portsInUse.map((probe) => probe.port).join(", ")}. Stop the conflicting process or change the port.`,
|
|
1723
|
+
status: "manual",
|
|
1724
|
+
blocking: false
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
const allCommands = { ...scan.presetCommands, ...scan.config?.config.commands ?? {} };
|
|
1728
|
+
for (const [index, entry] of (scan.config?.config.setupGuide ?? []).entries()) {
|
|
1729
|
+
if (typeof entry === "string") {
|
|
1730
|
+
steps.push({
|
|
1731
|
+
id: `guide-${index}`,
|
|
1732
|
+
title: entry,
|
|
1733
|
+
description: "From the project setup guide.",
|
|
1734
|
+
status: "manual",
|
|
1735
|
+
blocking: false
|
|
1736
|
+
});
|
|
1737
|
+
} else {
|
|
1738
|
+
const step = entry;
|
|
1739
|
+
let action;
|
|
1740
|
+
if (step.command !== void 0 && step.command in allCommands) {
|
|
1741
|
+
action = { kind: "run-command", label: "Run", target: step.command };
|
|
1742
|
+
} else if (step.script !== void 0) {
|
|
1743
|
+
action = { kind: "run-script", label: "Run", target: step.script };
|
|
1744
|
+
}
|
|
1745
|
+
steps.push({
|
|
1746
|
+
id: `guide-${index}`,
|
|
1747
|
+
title: step.title,
|
|
1748
|
+
description: step.description ?? "From the project setup guide.",
|
|
1749
|
+
status: action !== void 0 ? "todo" : "manual",
|
|
1750
|
+
blocking: false,
|
|
1751
|
+
action
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const docs = scan.config?.config.docs;
|
|
1756
|
+
if (typeof docs === "string" && docs.length > 0 && isSafeHttpUrl(docs)) {
|
|
1757
|
+
steps.push({
|
|
1758
|
+
id: "read-docs",
|
|
1759
|
+
title: "Read the project docs",
|
|
1760
|
+
description: docs,
|
|
1761
|
+
status: "manual",
|
|
1762
|
+
blocking: false,
|
|
1763
|
+
action: { kind: "open-docs", label: "Open docs", target: docs }
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
const startAction = pickStartAction(scan);
|
|
1767
|
+
if (startAction !== null) {
|
|
1768
|
+
steps.push({
|
|
1769
|
+
id: "start-app",
|
|
1770
|
+
title: "Start the app",
|
|
1771
|
+
description: "Run the development server once setup is complete.",
|
|
1772
|
+
status: "todo",
|
|
1773
|
+
blocking: false,
|
|
1774
|
+
action: startAction
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
const blocking = steps.filter((step) => step.blocking);
|
|
1778
|
+
const blockingDone = blocking.filter((step) => step.status === "done");
|
|
1779
|
+
const readiness = blocking.length === 0 ? 100 : Math.round(blockingDone.length / blocking.length * 100);
|
|
1780
|
+
const ready = readiness === 100;
|
|
1781
|
+
const remaining = blocking.length - blockingDone.length;
|
|
1782
|
+
const summary = ready ? "Project is ready to run." : `${remaining} setup step${remaining === 1 ? "" : "s"} remaining before the project is ready.`;
|
|
1783
|
+
return { steps, readiness, ready, summary };
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// src/cli/commands/onboard.ts
|
|
1787
|
+
function statusGlyph(status) {
|
|
1788
|
+
if (status === "done") {
|
|
1789
|
+
return pc3.green("[x]");
|
|
1790
|
+
}
|
|
1791
|
+
if (status === "todo") {
|
|
1792
|
+
return pc3.yellow("[ ]");
|
|
1793
|
+
}
|
|
1794
|
+
return pc3.cyan("[~]");
|
|
1795
|
+
}
|
|
1796
|
+
async function onboardCommand(cwd = process.cwd()) {
|
|
1797
|
+
const scan = await scanProject(cwd);
|
|
1798
|
+
const warnings = await runDoctor(cwd, scan);
|
|
1799
|
+
const plan = buildOnboardingPlan(scan, warnings);
|
|
1800
|
+
console.log(pc3.bold(`Onboarding ${safeDisplayText(scan.projectName)}`));
|
|
1801
|
+
console.log(`${plan.readiness}% ready \u2014 ${safeDisplayText(plan.summary)}`);
|
|
1802
|
+
console.log("");
|
|
1803
|
+
if (plan.steps.length === 0) {
|
|
1804
|
+
console.log(pc3.green("No onboarding steps detected."));
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
for (const step of plan.steps) {
|
|
1808
|
+
console.log(`${statusGlyph(step.status)} ${pc3.bold(safeDisplayText(step.title))}`);
|
|
1809
|
+
console.log(` ${safeDisplayText(step.description)}`);
|
|
1810
|
+
if (step.action && step.status !== "done") {
|
|
1811
|
+
console.log(pc3.dim(` \u2192 ${safeDisplayText(step.action.label)}`));
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/cli/commands/run.ts
|
|
1817
|
+
import pc4 from "picocolors";
|
|
1818
|
+
|
|
1555
1819
|
// src/core/process/runner.ts
|
|
1556
1820
|
import spawn2 from "cross-spawn";
|
|
1557
1821
|
|
|
@@ -1765,7 +2029,7 @@ async function runCommand(script, cwd = process.cwd()) {
|
|
|
1765
2029
|
const configuredCommand = scan.config?.config.commands?.[script] ?? scan.presetCommands[script];
|
|
1766
2030
|
if (configuredCommand !== void 0) {
|
|
1767
2031
|
if (isDangerousCommand(configuredCommand)) {
|
|
1768
|
-
console.error(
|
|
2032
|
+
console.error(pc4.red(`Refusing to run dangerous command "${safeDisplayText(script)}".`));
|
|
1769
2033
|
process.exitCode = 1;
|
|
1770
2034
|
return;
|
|
1771
2035
|
}
|
|
@@ -1782,17 +2046,17 @@ async function runCommand(script, cwd = process.cwd()) {
|
|
|
1782
2046
|
...Object.keys(scan.presetCommands)
|
|
1783
2047
|
];
|
|
1784
2048
|
const hint = available.length > 0 ? ` Available commands: ${safeDisplayList(available)}.` : "";
|
|
1785
|
-
console.error(
|
|
2049
|
+
console.error(pc4.red(`Command "${safeDisplayText(script)}" was not found.${hint}`));
|
|
1786
2050
|
process.exitCode = 1;
|
|
1787
2051
|
}
|
|
1788
2052
|
|
|
1789
2053
|
// src/cli/commands/scan.ts
|
|
1790
|
-
import
|
|
2054
|
+
import pc5 from "picocolors";
|
|
1791
2055
|
function formatList(values) {
|
|
1792
2056
|
return safeDisplayList(values);
|
|
1793
2057
|
}
|
|
1794
2058
|
function printScanResult(scan) {
|
|
1795
|
-
console.log(
|
|
2059
|
+
console.log(pc5.bold(`Project: ${safeDisplayText(scan.projectName)}`));
|
|
1796
2060
|
console.log(`Language: ${formatList(scan.language.detected) || "unknown"}`);
|
|
1797
2061
|
console.log(`Type: ${safeDisplayText(scan.framework?.type ?? "Unknown")}`);
|
|
1798
2062
|
console.log(`Manager: ${safeDisplayText(scan.packageManager ?? "unknown")}`);
|
|
@@ -1819,7 +2083,7 @@ async function scanCommand(cwd = process.cwd()) {
|
|
|
1819
2083
|
}
|
|
1820
2084
|
|
|
1821
2085
|
// src/cli/commands/start.ts
|
|
1822
|
-
import
|
|
2086
|
+
import pc6 from "picocolors";
|
|
1823
2087
|
|
|
1824
2088
|
// node_modules/open/index.js
|
|
1825
2089
|
import process7 from "process";
|
|
@@ -2634,7 +2898,10 @@ var WorkspaceRegistry = class {
|
|
|
2634
2898
|
}
|
|
2635
2899
|
async write(entries) {
|
|
2636
2900
|
await fs19.mkdir(path15.dirname(this.filePath), { recursive: true });
|
|
2637
|
-
await fs19.writeFile(this.filePath, JSON.stringify(entries, null, 2) + "\n",
|
|
2901
|
+
await fs19.writeFile(this.filePath, JSON.stringify(entries, null, 2) + "\n", {
|
|
2902
|
+
encoding: "utf8",
|
|
2903
|
+
mode: 384
|
|
2904
|
+
});
|
|
2638
2905
|
}
|
|
2639
2906
|
async seedFromEnv() {
|
|
2640
2907
|
if (this.seeded) {
|
|
@@ -2721,7 +2988,7 @@ import path16 from "path";
|
|
|
2721
2988
|
import spawn4 from "cross-spawn";
|
|
2722
2989
|
|
|
2723
2990
|
// src/version.ts
|
|
2724
|
-
var DEV_SURFACE_VERSION = "0.
|
|
2991
|
+
var DEV_SURFACE_VERSION = "0.7.0";
|
|
2725
2992
|
|
|
2726
2993
|
// src/server/localAccess.ts
|
|
2727
2994
|
var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
@@ -3082,6 +3349,11 @@ function handleDockerError(error, context) {
|
|
|
3082
3349
|
}
|
|
3083
3350
|
throw error;
|
|
3084
3351
|
}
|
|
3352
|
+
async function onboardingForRoot(root) {
|
|
3353
|
+
const scan = await scanProject(root);
|
|
3354
|
+
const warnings = await runDoctor(root, scan);
|
|
3355
|
+
return buildOnboardingPlan(scan, warnings);
|
|
3356
|
+
}
|
|
3085
3357
|
function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
3086
3358
|
app.get("/api/workspaces/:id/project", async (context) => {
|
|
3087
3359
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
@@ -3093,6 +3365,11 @@ function registerWorkspaceRoutes(app, resolveWorkspace) {
|
|
|
3093
3365
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
3094
3366
|
return context.json(await runDoctor(ws.root));
|
|
3095
3367
|
});
|
|
3368
|
+
app.get("/api/workspaces/:id/onboarding", async (context) => {
|
|
3369
|
+
const ws = await resolveWorkspace(context.req.param("id"));
|
|
3370
|
+
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
3371
|
+
return context.json(await onboardingForRoot(ws.root));
|
|
3372
|
+
});
|
|
3096
3373
|
app.get("/api/workspaces/:id/processes", async (context) => {
|
|
3097
3374
|
const ws = await resolveWorkspace(context.req.param("id"));
|
|
3098
3375
|
if (!ws) return context.json({ error: "Workspace not found." }, 404);
|
|
@@ -3315,6 +3592,11 @@ function registerHubApiRoutes(app, options) {
|
|
|
3315
3592
|
if (entries.length === 0) return context.json({ error: "No workspaces registered." }, 404);
|
|
3316
3593
|
return context.json(await runDoctor(hub.ensure(entries[0]).root));
|
|
3317
3594
|
});
|
|
3595
|
+
app.get("/api/onboarding", async (context) => {
|
|
3596
|
+
const entries = await hub.registry.list();
|
|
3597
|
+
if (entries.length === 0) return context.json({ error: "No workspaces registered." }, 404);
|
|
3598
|
+
return context.json(await onboardingForRoot(hub.ensure(entries[0]).root));
|
|
3599
|
+
});
|
|
3318
3600
|
app.get("/api/processes", async (context) => {
|
|
3319
3601
|
const entries = await hub.registry.list();
|
|
3320
3602
|
if (entries.length === 0) return context.json([]);
|
|
@@ -3418,6 +3700,17 @@ function setupHubWebSocket(server, hub) {
|
|
|
3418
3700
|
}
|
|
3419
3701
|
|
|
3420
3702
|
// src/server/index.ts
|
|
3703
|
+
function warnIfContainerRootsUnset(host) {
|
|
3704
|
+
if (host !== "0.0.0.0" && host !== "::") {
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
if (process.env.DEVSURFACE_WORKSPACE_ROOTS?.trim()) {
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
console.warn(
|
|
3711
|
+
"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."
|
|
3712
|
+
);
|
|
3713
|
+
}
|
|
3421
3714
|
async function fileExists(filePath) {
|
|
3422
3715
|
try {
|
|
3423
3716
|
await fs21.access(filePath);
|
|
@@ -3533,6 +3826,7 @@ async function startHubServer(options) {
|
|
|
3533
3826
|
const port = options.port ?? DEFAULT_PORT;
|
|
3534
3827
|
const hub = new Hub({ dataDir: options.dataDir });
|
|
3535
3828
|
hub.attachCleanupHandlers();
|
|
3829
|
+
warnIfContainerRootsUnset(host);
|
|
3536
3830
|
if (options.initialWorkspace) {
|
|
3537
3831
|
await hub.registry.add(options.initialWorkspace);
|
|
3538
3832
|
}
|
|
@@ -3608,7 +3902,7 @@ function dashboardUrl(workspaceId2, port = DEFAULT_PORT, host = DEFAULT_HOST) {
|
|
|
3608
3902
|
async function startCommand(options) {
|
|
3609
3903
|
const cwd = options.cwd ?? process.cwd();
|
|
3610
3904
|
const port = options.port ?? 4567;
|
|
3611
|
-
console.log(
|
|
3905
|
+
console.log(pc6.bold(`DevSurface v${DEV_SURFACE_VERSION}`));
|
|
3612
3906
|
console.log("Scanning project...\n");
|
|
3613
3907
|
const scan = await scanProject(cwd);
|
|
3614
3908
|
printScanResult(scan);
|
|
@@ -3616,7 +3910,7 @@ async function startCommand(options) {
|
|
|
3616
3910
|
if (warnings.length > 0) {
|
|
3617
3911
|
console.log("\nWarnings:");
|
|
3618
3912
|
for (const item of warnings) {
|
|
3619
|
-
const marker = item.severity === "error" ?
|
|
3913
|
+
const marker = item.severity === "error" ? pc6.red("!") : pc6.yellow("!");
|
|
3620
3914
|
console.log(` ${marker} ${item.title}`);
|
|
3621
3915
|
}
|
|
3622
3916
|
}
|
|
@@ -3625,8 +3919,8 @@ async function startCommand(options) {
|
|
|
3625
3919
|
const registered = await registerWorkspaceRemotely(cwd, port);
|
|
3626
3920
|
if (registered) {
|
|
3627
3921
|
const url = dashboardUrl(registered.id, port);
|
|
3628
|
-
console.log(`Workspace ${
|
|
3629
|
-
console.log(`Dashboard -> ${
|
|
3922
|
+
console.log(`Workspace ${pc6.cyan(registered.name)} attached.`);
|
|
3923
|
+
console.log(`Dashboard -> ${pc6.cyan(url)}`);
|
|
3630
3924
|
if (options.openBrowser !== false) {
|
|
3631
3925
|
await open_default(url);
|
|
3632
3926
|
}
|
|
@@ -3640,13 +3934,13 @@ async function startCommand(options) {
|
|
|
3640
3934
|
initialWorkspace: cwd
|
|
3641
3935
|
});
|
|
3642
3936
|
console.log(`
|
|
3643
|
-
Dashboard running at -> ${
|
|
3937
|
+
Dashboard running at -> ${pc6.cyan(server.url)}`);
|
|
3644
3938
|
}
|
|
3645
3939
|
|
|
3646
3940
|
// src/cli/commands/serve.ts
|
|
3647
|
-
import
|
|
3941
|
+
import pc7 from "picocolors";
|
|
3648
3942
|
async function serveCommand(options) {
|
|
3649
|
-
console.log(
|
|
3943
|
+
console.log(pc7.bold(`DevSurface Hub v${DEV_SURFACE_VERSION}`));
|
|
3650
3944
|
console.log("Starting hub server...\n");
|
|
3651
3945
|
const server = await startHubServer({
|
|
3652
3946
|
port: options.port,
|
|
@@ -3656,7 +3950,7 @@ async function serveCommand(options) {
|
|
|
3656
3950
|
if (summaries.length > 0) {
|
|
3657
3951
|
console.log(`Registered workspaces: ${summaries.length}`);
|
|
3658
3952
|
for (const ws of summaries) {
|
|
3659
|
-
console.log(` ${
|
|
3953
|
+
console.log(` ${pc7.cyan(ws.name)} -> ${ws.path}`);
|
|
3660
3954
|
}
|
|
3661
3955
|
} else {
|
|
3662
3956
|
console.log(
|
|
@@ -3664,17 +3958,17 @@ async function serveCommand(options) {
|
|
|
3664
3958
|
);
|
|
3665
3959
|
}
|
|
3666
3960
|
console.log(`
|
|
3667
|
-
Hub running at -> ${
|
|
3961
|
+
Hub running at -> ${pc7.cyan(server.url)}`);
|
|
3668
3962
|
}
|
|
3669
3963
|
|
|
3670
3964
|
// src/cli/commands/workspace.ts
|
|
3671
3965
|
import path18 from "path";
|
|
3672
|
-
import
|
|
3966
|
+
import pc8 from "picocolors";
|
|
3673
3967
|
async function workspaceAddCommand(dirPath) {
|
|
3674
3968
|
const registry = new WorkspaceRegistry();
|
|
3675
3969
|
const target = path18.resolve(dirPath ?? process.cwd());
|
|
3676
3970
|
const entry = await registry.add(target);
|
|
3677
|
-
console.log(`Added workspace ${
|
|
3971
|
+
console.log(`Added workspace ${pc8.cyan(entry.name)} (${entry.id}) -> ${entry.path}`);
|
|
3678
3972
|
}
|
|
3679
3973
|
async function workspaceListCommand() {
|
|
3680
3974
|
const registry = new WorkspaceRegistry();
|
|
@@ -3688,7 +3982,7 @@ async function workspaceListCommand() {
|
|
|
3688
3982
|
console.log(`${entries.length} workspace${entries.length === 1 ? "" : "s"}:
|
|
3689
3983
|
`);
|
|
3690
3984
|
for (const entry of entries) {
|
|
3691
|
-
console.log(` ${
|
|
3985
|
+
console.log(` ${pc8.cyan(entry.name)} (${entry.id})`);
|
|
3692
3986
|
console.log(` ${entry.path}`);
|
|
3693
3987
|
}
|
|
3694
3988
|
}
|
|
@@ -3696,7 +3990,7 @@ async function workspaceRemoveCommand(id) {
|
|
|
3696
3990
|
const registry = new WorkspaceRegistry();
|
|
3697
3991
|
const removed = await registry.remove(id);
|
|
3698
3992
|
if (removed) {
|
|
3699
|
-
console.log(`Removed workspace ${
|
|
3993
|
+
console.log(`Removed workspace ${pc8.cyan(id)}.`);
|
|
3700
3994
|
} else {
|
|
3701
3995
|
console.error(`Workspace "${id}" not found.`);
|
|
3702
3996
|
process.exitCode = 1;
|
|
@@ -3826,6 +4120,9 @@ program.command("scan").description("Print detected project info.").action(() =>
|
|
|
3826
4120
|
program.command("doctor").description("Print setup health warnings.").action(() => {
|
|
3827
4121
|
handle(doctorCommand(process.cwd()));
|
|
3828
4122
|
});
|
|
4123
|
+
program.command("onboard").description("Print a guided setup checklist with readiness score.").action(() => {
|
|
4124
|
+
handle(onboardCommand(process.cwd()));
|
|
4125
|
+
});
|
|
3829
4126
|
program.command("init").description("Create a starter devsurface.config.json.").action(() => {
|
|
3830
4127
|
handle(initCommand(process.cwd()));
|
|
3831
4128
|
});
|