@tempo-ai/mcp 0.0.103 → 0.0.104
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/dist/bin.js +2 -2
- package/dist/{call-XYOJSSYR.js → call-5BVCQ6WY.js} +3 -3
- package/dist/{chunk-C3J4LFJI.js → chunk-2MN4FILH.js} +6 -6
- package/dist/{chunk-KQNQSVOA.js → chunk-KFUOQZBJ.js} +1 -1
- package/dist/{chunk-KQNQSVOA.js.map → chunk-KFUOQZBJ.js.map} +1 -1
- package/dist/index.js +2 -2
- package/dist/{serve-P7M4UFJS.js → serve-FWQ5ZBR7.js} +690 -110
- package/dist/serve-FWQ5ZBR7.js.map +1 -0
- package/dist/{token-IDV6YJ2Q.js → token-YG7CTDI7.js} +2 -2
- package/package.json +1 -1
- package/dist/serve-P7M4UFJS.js.map +0 -1
- /package/dist/{call-XYOJSSYR.js.map → call-5BVCQ6WY.js.map} +0 -0
- /package/dist/{chunk-C3J4LFJI.js.map → chunk-2MN4FILH.js.map} +0 -0
- /package/dist/{token-IDV6YJ2Q.js.map → token-YG7CTDI7.js.map} +0 -0
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
readAuthSync,
|
|
16
16
|
resolveConvexSiteUrl,
|
|
17
17
|
resolveConvexUrl
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-KFUOQZBJ.js";
|
|
19
19
|
|
|
20
20
|
// ../../node_modules/.pnpm/cronstrue@3.24.0/node_modules/cronstrue/dist/cronstrue.js
|
|
21
21
|
var require_cronstrue = __commonJS({
|
|
@@ -7820,6 +7820,46 @@ async function assertCanvasEndpointsShareable(endpoints, checkCanvasPushState) {
|
|
|
7820
7820
|
});
|
|
7821
7821
|
}
|
|
7822
7822
|
}
|
|
7823
|
+
async function assertCanvasEndpointsHavePreviews(endpoints, deps) {
|
|
7824
|
+
const ensure = deps.ensureCanvasPreview;
|
|
7825
|
+
if (!ensure) return;
|
|
7826
|
+
for (const endpoint of endpoints) {
|
|
7827
|
+
if (!endpoint || typeof endpoint !== "object") continue;
|
|
7828
|
+
const ep = endpoint;
|
|
7829
|
+
if (ep.type !== "canvas") continue;
|
|
7830
|
+
if (!ep.orgProjectId || !ep.canvasPath || !ep.branch) continue;
|
|
7831
|
+
let hasPreview = false;
|
|
7832
|
+
try {
|
|
7833
|
+
const snapshot = await deps.convex.query(
|
|
7834
|
+
api.canvasIndex.getCanvasPreviewSnapshot,
|
|
7835
|
+
{
|
|
7836
|
+
orgProjectId: ep.orgProjectId,
|
|
7837
|
+
canvasPath: ep.canvasPath,
|
|
7838
|
+
branch: ep.branch
|
|
7839
|
+
}
|
|
7840
|
+
);
|
|
7841
|
+
hasPreview = (snapshot?.storyboards?.length ?? 0) > 0;
|
|
7842
|
+
} catch {
|
|
7843
|
+
hasPreview = false;
|
|
7844
|
+
}
|
|
7845
|
+
if (hasPreview) continue;
|
|
7846
|
+
const result = await ensure({
|
|
7847
|
+
orgProjectId: ep.orgProjectId,
|
|
7848
|
+
canvasPath: ep.canvasPath,
|
|
7849
|
+
branch: ep.branch
|
|
7850
|
+
});
|
|
7851
|
+
if (result.ok) continue;
|
|
7852
|
+
throw new McpToolError({
|
|
7853
|
+
code: "canvas_preview_capture_failed",
|
|
7854
|
+
message: `Cannot link canvas "${ep.canvasPath}": a linked canvas must have a rendered preview so it is reviewable from the issue/doc it is linked to, and capturing one failed: ${result.message} Fix the capture (make sure the canvas devserver is running and the canvas's storyboards render \u2014 canvas_screenshot is a quick check), then retry link_items. No link was created.`,
|
|
7855
|
+
details: {
|
|
7856
|
+
canvasPath: ep.canvasPath,
|
|
7857
|
+
branch: ep.branch,
|
|
7858
|
+
captureFailure: result.message
|
|
7859
|
+
}
|
|
7860
|
+
});
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7823
7863
|
async function resolveCanvasEndpoint(endpoint, resolve2) {
|
|
7824
7864
|
if (!resolve2) return endpoint;
|
|
7825
7865
|
const ep = endpoint;
|
|
@@ -7900,7 +7940,7 @@ function registerArtifactTools(server, deps) {
|
|
|
7900
7940
|
const { convex, getWritePermission } = deps;
|
|
7901
7941
|
server.tool(
|
|
7902
7942
|
"link_items",
|
|
7903
|
-
"Create a bidirectional link between two items in ONE call. Endpoint types: 'issue' (issueId \u2014 a human id like TEM-42 or a Convex id; either works), 'doc' (cloud doc \u2014 pass docId from docs_create), 'canvas', or 'branch' (orgProjectId + branchName). Links point at a BRANCH, never at one person's workspace, so everyone in the org sees the work. A 'workspace' endpoint (orgProjectId + localBranch) is still accepted and is TRANSLATED to the branch it sits on. Issue, doc, workspace, and branch links are created immediately. To link a CANVAS, pass just { type: 'canvas', canvasPath: '<slug, folder, or .canvas.tsx path>' } \u2014 the slug you gave canvas_create, or the canvas_dir/file_path returned by canvas_list. The server fills in the project id and git branch from the active workspace and normalizes the path. Canvas endpoints must match the active workspace project and their named branch must be positively verified on that repository's remote; the active branch must also be clean with no unpushed commits. Otherwise the tool returns branch_not_pushed and creates no link. Commit and push the canvas, then retry link_items so collaborators can fetch it.",
|
|
7943
|
+
"Create a bidirectional link between two items in ONE call. Endpoint types: 'issue' (issueId \u2014 a human id like TEM-42 or a Convex id; either works), 'doc' (cloud doc \u2014 pass docId from docs_create), 'canvas', or 'branch' (orgProjectId + branchName). Links point at a BRANCH, never at one person's workspace, so everyone in the org sees the work. A 'workspace' endpoint (orgProjectId + localBranch) is still accepted and is TRANSLATED to the branch it sits on. Issue, doc, workspace, and branch links are created immediately. To link a CANVAS, pass just { type: 'canvas', canvasPath: '<slug, folder, or .canvas.tsx path>' } \u2014 the slug you gave canvas_create, or the canvas_dir/file_path returned by canvas_list. The server fills in the project id and git branch from the active workspace and normalizes the path. Canvas endpoints must match the active workspace project and their named branch must be positively verified on that repository's remote; the active branch must also be clean with no unpushed commits. Otherwise the tool returns branch_not_pushed and creates no link. Commit and push the canvas, then retry link_items so collaborators can fetch it. A linked canvas must also have a rendered PREVIEW: when none exists yet for the exact branch, the tool captures one at link time (the canvas devserver must be able to render the storyboards); if that capture fails it returns canvas_preview_capture_failed with the reason and creates no link \u2014 fix the render, then retry.",
|
|
7904
7944
|
{
|
|
7905
7945
|
endpointA: coercibleEndpointSchema(),
|
|
7906
7946
|
endpointB: coercibleEndpointSchema()
|
|
@@ -7929,6 +7969,7 @@ function registerArtifactTools(server, deps) {
|
|
|
7929
7969
|
[endpointA, endpointB],
|
|
7930
7970
|
deps.checkCanvasPushState
|
|
7931
7971
|
);
|
|
7972
|
+
await assertCanvasEndpointsHavePreviews([endpointA, endpointB], deps);
|
|
7932
7973
|
const linkId = await convex.mutation(api.artifact_links.createLink, {
|
|
7933
7974
|
orgId: ctx.orgId,
|
|
7934
7975
|
endpointA,
|
|
@@ -8912,6 +8953,7 @@ function createIssuesMcpServer(context) {
|
|
|
8912
8953
|
convex,
|
|
8913
8954
|
getWritePermission,
|
|
8914
8955
|
checkCanvasPushState: context.checkCanvasPushState,
|
|
8956
|
+
ensureCanvasPreview: context.ensureCanvasPreview,
|
|
8915
8957
|
resolveCanvasLinkEndpoint: context.resolveCanvasLinkEndpoint,
|
|
8916
8958
|
getWorkspaceEndpoint: context.getWorkspaceEndpoint,
|
|
8917
8959
|
getOriginChatTabId: context.getOriginChatTabId,
|
|
@@ -15519,12 +15561,13 @@ scaffolded project.
|
|
|
15519
15561
|
| \`scripts.install\` | string | Command to install dependencies (runs from the tempo root). A plain package-manager install (bare \`npm install\`/\`pnpm install\`/\u2026 or Tempo's chained \`cd .. && \u2026 && cd tempo && \u2026\` form) means "use Tempo's default install behavior": Tempo plans its own project-root + tempo-root installs with the detected package manager and the exact string is not executed. Anything else is a CUSTOM command \u2014 it runs verbatim from the tempo root, must cover BOTH roots' dependencies itself, and **Tempo never rewrites it**, so an edit you make here sticks. Only write a custom command when the default behavior actually fails (e.g. a \`root_deps_missing\` error on an unusual monorepo). |
|
|
15520
15562
|
| \`scripts.start\` | string | Canvas-host launcher (runs from the tempo root). Prefer a foreground process. Tempo substitutes every literal \`\${PORT}\`, exports the same port as \`TEMPO_PORT\`, and sets \`TEMPO=true\`. |
|
|
15521
15563
|
| \`scripts.stop\` | string? | Matching teardown for a daemonized/external host. Tempo substitutes \`\${PORT}\` and runs this before signalling the launcher group. Required when \`scripts.start\` exits successfully while the host continues in the background. |
|
|
15522
|
-
| \`urls.tempoHost\` | URL
|
|
15523
|
-
| \`apps\` | array? | \`[{ "appDir"?, "start"?, "url"? }]\` \u2014 one entry per client app; only \`apps[0]\` is active today. \`start\` launches the user's REAL app for route storyboards
|
|
15564
|
+
| \`urls.tempoHost\` | URL source? | Public \`/tempo-host\` URL to probe when stdout cannot describe the reachable boundary (containers, tunnels, remotes). Either an HTTP(S) string (which may contain \`\${PORT}\`) or \`{ "command": "..." }\`; the command runs once per launch and must print exactly one HTTP(S) URL. |
|
|
15565
|
+
| \`apps\` | array? | \`[{ "appDir"?, "start"?, "stop"?, "url"? }]\` \u2014 one entry per client app; only \`apps[0]\` is active today. \`start\` launches the user's REAL app for route storyboards. \`url\` is either an HTTP(S) string or \`{ "command": "..." }\`; a command runs once per launch and must print exactly one HTTP(S) URL. \`stop\` supplies teardown authority when \`start\` launches a container, tunnel, daemon, or remote workspace and exits. \`\${APP_PORT}\` is an optional allocated-port helper and legacy \`\${PORT}\` remains supported in all three fields. Legacy \`apps[].appStart\`, \`scripts.appStart\`, and top-level \`appDir\` still read and auto-migrate. |
|
|
15524
15566
|
|
|
15525
15567
|
**Every command in this file runs through the platform shell \u2014 \`cmd.exe\` on
|
|
15526
15568
|
Windows, not PowerShell.** That applies to \`scripts.install\` and
|
|
15527
|
-
\`scripts.start\` exactly as it does to app \`start
|
|
15569
|
+
\`scripts.start\` exactly as it does to app \`start\`, \`stop\`, and
|
|
15570
|
+
\`url.command\` and \`urls.tempoHost.command\`. Write plain, cross-platform
|
|
15528
15571
|
invocations; \`&&\` and \`cd\` are fine (cmd supports both). Never use PowerShell
|
|
15529
15572
|
syntax \u2014 \`if (Test-Path \u2026) { \u2026 } else { \u2026 }\`, \`Test-Path\`, \`$env:FOO\` \u2014 because
|
|
15530
15573
|
cmd.exe cannot parse it and aborts with "X was unexpected at this time" BEFORE
|
|
@@ -15665,7 +15708,7 @@ When Tempo starts the devserver (via the \`scripts.start\` command), it sets:
|
|
|
15665
15708
|
- \`NODE_ENV=development\` \u2014 ensures React dev mode (needed for fiber-based element selection)
|
|
15666
15709
|
- \`TEMPO=true\` \u2014 activates Tempo's framework-specific annotations: \`tempoVitePlugin()\` for Vite, \`tempoNextjsPlugin()\` for Next.js, and \`tempoExpoBabelPlugin\` for Expo web
|
|
15667
15710
|
|
|
15668
|
-
The host URL is detected from local URLs printed by the framework server; \`urls.tempoHost\`, when configured, is probed alongside the printed URL and wins whenever the printed URL is not reachable from Tempo (containers, tunnels, remotes). A literal \`urls.tempoHost\` (no \`\${PORT}\` token) is additionally probed without waiting for any stdout URL at all. Vite, Next.js, and Expo web can all use dynamic ports; agents should not hard-code a port into Expo setup unless the user explicitly asks for one.
|
|
15711
|
+
The host URL is detected from local URLs printed by the framework server; \`urls.tempoHost\`, when configured, is probed alongside the printed URL and wins whenever the printed URL is not reachable from Tempo (containers, tunnels, remotes). A literal \`urls.tempoHost\` (no \`\${PORT}\` token) is additionally probed without waiting for any stdout URL at all. A command-backed \`urls.tempoHost\` executes exactly once with \`TEMPO_PORT\` and \`PORT\` after spawn; when \`scripts.stop\` is configured it waits for the launcher to exit cleanly first, then the resolved URL owns liveness and stop owns teardown. Vite, Next.js, and Expo web can all use dynamic ports; agents should not hard-code a port into Expo setup unless the user explicitly asks for one.
|
|
15669
15712
|
|
|
15670
15713
|
#### Customizing the launcher \u2014 decision ladder
|
|
15671
15714
|
|
|
@@ -15682,6 +15725,8 @@ Work down and stop at the FIRST line that fits; change nothing beyond it:
|
|
|
15682
15725
|
with the reachable \`/tempo-host\` URL (use \`\${PORT}\` in it unless the
|
|
15683
15726
|
boundary fixes the port), and forward \`TEMPO\` and \`TEMPO_PORT\` across the
|
|
15684
15727
|
boundary.
|
|
15728
|
+
If the boundary chooses its URL dynamically, use
|
|
15729
|
+
\`{ "command": "..." }\`; the command's entire stdout must be that one URL.
|
|
15685
15730
|
4. **Launcher exits after starting the real server** (e.g.
|
|
15686
15731
|
\`docker compose up --detach\`)? Add \`scripts.stop\` as the matched
|
|
15687
15732
|
teardown \u2014 without it the start fails with
|
|
@@ -15696,7 +15741,9 @@ Work down and stop at the FIRST line that fits; change nothing beyond it:
|
|
|
15696
15741
|
attempt \u2014 unless \`urls.tempoHost\` spells a literal port (no \`\${PORT}\`
|
|
15697
15742
|
token), which pins every attempt to that port. Either way it substitutes
|
|
15698
15743
|
every literal \`\${PORT}\` in \`scripts.start\`, \`scripts.stop\`, and
|
|
15699
|
-
\`urls.tempoHost\`, and always exports
|
|
15744
|
+
a string \`urls.tempoHost\` or \`urls.tempoHost.command\`, and always exports
|
|
15745
|
+
the chosen number as \`TEMPO_PORT\` (and as \`PORT\` when an explicit URL
|
|
15746
|
+
source participates in launch).
|
|
15700
15747
|
Custom launchers should read \`TEMPO_PORT\` or use the token; do not assume
|
|
15701
15748
|
Tempo will overwrite a generic ambient \`PORT\` variable.
|
|
15702
15749
|
- \`scripts.start\` should normally stay in the foreground. Tempo then owns the
|
|
@@ -15920,7 +15967,7 @@ export default function AuthComponentsCanvas() {
|
|
|
15920
15967
|
|
|
15921
15968
|
Canvases created before the React format use object exports instead of JSX \u2014 \`export const Login: TempoStoryboard = { render: () => <LoginForm />, layout: {\u2026} }\` with a \`TempoCanvasConfig\` default export. Tempo migrates these to the React format automatically when they are opened in the Design tab. If you encounter one, run \`canvas_migrate_to_latest\` on it BEFORE making any edit \u2014 never hand-write legacy declarations, and never mix the two formats in one file.
|
|
15922
15969
|
|
|
15923
|
-
### Route Storyboards & the app dev config (\`apps[0].start\` / \`url\`)
|
|
15970
|
+
### Route Storyboards & the app dev config (\`apps[0].start\` / \`stop\` / \`url\`)
|
|
15924
15971
|
|
|
15925
15972
|
Route storyboards (\`<RouteStoryboard>\`) load **the user's real app** in the iframe, NOT the canvas sidecar. They need a second supervised process \u2014 the user's actual app dev server \u2014 which Tempo manages from the app dev command in \`tempo.config.json\`, stored canonically on the \`apps[]\` list (one entry per client app; only \`apps[0]\` is active today):
|
|
15926
15973
|
|
|
@@ -15933,14 +15980,34 @@ Route storyboards (\`<RouteStoryboard>\`) load **the user's real app** in the if
|
|
|
15933
15980
|
"apps": [
|
|
15934
15981
|
{
|
|
15935
15982
|
"appDir": "client", // app package dir, relative to repo root (omit for autodetect)
|
|
15936
|
-
"start": "
|
|
15937
|
-
"
|
|
15983
|
+
"start": "make remote-workspace", // foreground server OR short-lived launcher
|
|
15984
|
+
"stop": "make stop-remote-workspace", // required when start exits after handoff
|
|
15985
|
+
"url": { "command": "make --silent show-workspace-url" } // or a literal HTTP(S) URL
|
|
15938
15986
|
}
|
|
15939
15987
|
]
|
|
15940
15988
|
}
|
|
15941
15989
|
\`\`\`
|
|
15942
15990
|
|
|
15943
|
-
(Files written by older Tempo versions may still carry \`apps[].appStart\`, \`scripts.appStart\`, or top-level \`appDir\`. They read normally and migrate to
|
|
15991
|
+
(Files written by older Tempo versions may still carry \`apps[].appStart\`, \`scripts.appStart\`, or top-level \`appDir\`. They read normally and migrate to \`apps[].start\` / \`apps[].appDir\` on the next config write.)
|
|
15992
|
+
|
|
15993
|
+
The three app commands share one launch context: Tempo runs them from
|
|
15994
|
+
\`appDir\` and exports the selected port as \`APP_PORT\`, \`PORT\`, and
|
|
15995
|
+
\`TEMPO_APP_PORT\` (literal \`\${APP_PORT}\` and legacy \`\${PORT}\` tokens are
|
|
15996
|
+
also substituted). A command-backed \`url\` runs exactly once after spawn; in
|
|
15997
|
+
the \`stop + url.command\` handoff shape, it waits for \`start\` to exit cleanly
|
|
15998
|
+
first so a URL getter cannot race remote provisioning. It is bounded and
|
|
15999
|
+
abortable, and its entire trimmed stdout must be one HTTP(S) URL; Tempo never
|
|
16000
|
+
guesses whether a string is executable. Readiness is always HTTP-probed at the
|
|
16001
|
+
resolved URL.
|
|
16002
|
+
|
|
16003
|
+
For an ordinary foreground dev server, the child process owns liveness and
|
|
16004
|
+
\`stop\` is optional. If \`start\` exits successfully before readiness, Tempo
|
|
16005
|
+
accepts the handoff only when \`stop\` is configured; the resolved URL then
|
|
16006
|
+
owns liveness, repeated probe failures demote the server, and \`stop\` owns
|
|
16007
|
+
teardown. Tempo persists the exact stop command, cwd, port, and resolved URL
|
|
16008
|
+
so the next session can reconcile a remote/container runtime after a hard
|
|
16009
|
+
crash instead of launching a duplicate. A failed URL command or readiness
|
|
16010
|
+
timeout after handoff also runs \`stop\` immediately.
|
|
15944
16011
|
|
|
15945
16012
|
**The app's own bundler config MUST include the Tempo annotation plugin.** Route storyboards are served by the user's app dev server (NOT the \`tempo/\` sidecar), so the canvas can only select/edit their elements if THAT server injects the \`data-tempo-*\` annotations \u2014 i.e. the **app's own** framework config carries the Tempo plugin, active under \`TEMPO=true\`. Without it the app still renders in the iframe, but every click collapses to one unmappable root and the user reports *"route storyboard elements are not clickable."* This is separate from the \`tempo/\` sidecar's plugin (\xA74.2) \u2014 both configs need it, for different servers. Add the plugin to the app package (and add \`tempo-sdk\` to the app's deps) per framework:
|
|
15946
16013
|
|
|
@@ -15998,8 +16065,8 @@ The plugin no-ops unless \`TEMPO=true\` (which the supervisor sets when it runs
|
|
|
15998
16065
|
Mechanics:
|
|
15999
16066
|
|
|
16000
16067
|
- The supervisor always allocates a free local TCP port per workspace and persists it at \`<workspaceRoot>/.tempo/app-devserver.json\` (gitignored, per-worktree).
|
|
16001
|
-
- A \`url\` containing \`\${APP_PORT}\` (or legacy \`\${PORT}\`) uses that allocated helper value. A literal URL is an opaque HTTP(S) readiness target and route base: its hostname and port never control local allocation, so it may point through a container, tunnel, or remote machine.
|
|
16002
|
-
- Every literal \`\${APP_PORT}\` or legacy \`\${PORT}\` in app \`start\` and \`url\` is substituted before
|
|
16068
|
+
- A string \`url\` containing \`\${APP_PORT}\` (or legacy \`\${PORT}\`) uses that allocated helper value. A literal URL is an opaque HTTP(S) readiness target and route base: its hostname and port never control local allocation, so it may point through a container, tunnel, or remote machine. A \`{ command }\` URL resolves the same target dynamically once per launch.
|
|
16069
|
+
- Every literal \`\${APP_PORT}\` or legacy \`\${PORT}\` in app \`start\`, \`stop\`, and \`url.command\` (or a string \`url\`) is substituted before execution/probing. The renderer resolves route storyboard paths against the resulting URL.
|
|
16003
16070
|
- Lifecycle: warm with a 5-minute idle stop \u2014 the supervisor stays alive while a canvas with route storyboards is open and stops 5 minutes after the last canvas closes.
|
|
16004
16071
|
- While starting, an overlay reads "Starting your app dev server\u2026" on every route storyboard.
|
|
16005
16072
|
|
|
@@ -16016,7 +16083,7 @@ Mechanics:
|
|
|
16016
16083
|
|
|
16017
16084
|
**When using the allocated helper port, invoke the bundler binary directly \u2014 do NOT route the port flag through \`<pm> run dev -- --port=\${APP_PORT}\`.** When the \`dev\` script is a bare \`vite\` (the common case) and the project uses **pnpm**, that form can forward the literal \`--\` separator through to Vite, which treats it as a positional arg and ignores \`--port\`. The direct-exec form (\`pnpm exec vite \u2026\` / \`npx vite \u2026\`) is correct under every package manager. (\`--strictPort\` makes a mismatch fail loudly.)
|
|
16018
16085
|
|
|
16019
|
-
The command is otherwise arbitrary. The allocated helper token is optional
|
|
16086
|
+
The command is otherwise arbitrary. The allocated helper token is optional. A configured \`url\` is always the readiness target Tempo probes and becomes the ongoing liveness authority after a clean launcher handoff.
|
|
16020
16087
|
|
|
16021
16088
|
**Independent readiness URL.** Use a literal \`url\` whenever the URL Tempo can reach differs from the locally allocated port, including a container, tunnel, or remote machine:
|
|
16022
16089
|
|
|
@@ -16031,9 +16098,14 @@ The command is otherwise arbitrary. The allocated helper token is optional, and
|
|
|
16031
16098
|
|
|
16032
16099
|
The supervisor probes \`url\` directly rather than waiting for stdout URL discovery and uses it as the public route base after it responds. The local helper port remains independently allocated and available to \`start\` through \`APP_PORT\`, \`PORT\`, and \`TEMPO_APP_PORT\`, even when the literal URL spells a completely different host or explicit port.
|
|
16033
16100
|
|
|
16034
|
-
|
|
16101
|
+
Use \`url: { command: "..." }\` when that target can only be discovered after
|
|
16102
|
+
\`start\`, such as a newly allocated remote workspace or tunnel. The explicit
|
|
16103
|
+
object is the execution boundary: plain URL strings are never treated as shell
|
|
16104
|
+
commands.
|
|
16105
|
+
|
|
16106
|
+
**Keep every command plain and cross-platform \u2014 one invocation, no shell conditionals.** Tempo spawns \`start\`, \`stop\`, and \`url.command\` through the platform shell (**cmd.exe on Windows**, not PowerShell) and substitutes \`\${APP_PORT}\` (or legacy \`\${PORT}\`) before spawning. A leading \`NAME=value\` prefix \u2014 for example \`PORT=\${APP_PORT} npm run dev\` \u2014 is also safe on every OS for \`start\`: the supervisor lifts it into the spawn environment instead of handing it to the shell, and it always sets \`APP_PORT\`, \`PORT\`, and \`TEMPO_APP_PORT\` in the environment regardless. Never use PowerShell syntax \u2014 \`if (Test-Path \u2026) { \u2026 } else { \u2026 }\`, \`Test-Path\`, \`$env:FOO\`, \`Get-\`/\`Set-\` cmdlets \u2014 cmd.exe can't parse it and aborts before running anything. If the app lives in a subdirectory, set \`appDir\` instead of changing directories inside the command.
|
|
16035
16107
|
|
|
16036
|
-
**Driving the lifecycle directly.** Three sibling tools mirror the canvas-sidecar trio: \`check_app_dev_server\` / \`start_app_dev_server\` / \`stop_app_dev_server\` (vs \`check_canvas_devserver\` / \`start_canvas_devserver\` / \`stop_canvas_devserver\`). \`check_app_dev_server\` returns \`{configured, running, phase, port, url, error}\` \u2014 call it first when a route storyboard isn't loading, to disambiguate: \`configured: false\` \u2192 \`set_app_dev_command\`; \`configured: true, running: false\` \u2192 read \`error\`, then \`start_app_dev_server\`; \`running: true\` \u2192 the route itself is the problem, not the supervisor. After editing the app dev command, \`stop_app_dev_server\` then \`start_app_dev_server\` to pick up the new command.
|
|
16108
|
+
**Driving the lifecycle directly.** Three sibling tools mirror the canvas-sidecar trio: \`check_app_dev_server\` / \`start_app_dev_server\` / \`stop_app_dev_server\` (vs \`check_canvas_devserver\` / \`start_canvas_devserver\` / \`stop_canvas_devserver\`). \`check_app_dev_server\` returns \`{configured, running, phase, port, url, livenessSource, error}\` \u2014 call it first when a route storyboard isn't loading, to disambiguate: \`configured: false\` \u2192 \`set_app_dev_command\`; \`configured: true, running: false\` \u2192 read \`error\`, then \`start_app_dev_server\`; \`running: true\` \u2192 the route itself is the problem, not the supervisor. \`livenessSource: "probe"\` identifies an external-runtime handoff; \`"child"\` identifies a foreground app process. After editing the app dev command, \`stop_app_dev_server\` then \`start_app_dev_server\` to pick up the new command.
|
|
16037
16109
|
|
|
16038
16110
|
**Pre-existing canvases with blank route iframes** on a project without an app dev command are a config gap, not a bug \u2014 call \`set_app_dev_command\` and the iframes light up.
|
|
16039
16111
|
|
|
@@ -24391,6 +24463,66 @@ async function atomicWriteText(filePath, content) {
|
|
|
24391
24463
|
}
|
|
24392
24464
|
}
|
|
24393
24465
|
|
|
24466
|
+
// ../tempo-devserver/http-url-source.ts
|
|
24467
|
+
var ANSI_ESCAPE_RE2 = /\x1b\[[0-9;]*m/g;
|
|
24468
|
+
async function resolveHttpUrlSource(options) {
|
|
24469
|
+
if (typeof options.source === "string") {
|
|
24470
|
+
return options.substitutePort(options.source, options.port);
|
|
24471
|
+
}
|
|
24472
|
+
const controller = new AbortController();
|
|
24473
|
+
const onAbort = () => controller.abort();
|
|
24474
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
24475
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
24476
|
+
let result;
|
|
24477
|
+
try {
|
|
24478
|
+
result = await runAbortableShellCommand({
|
|
24479
|
+
command: options.substitutePort(options.source.command, options.port),
|
|
24480
|
+
cwd: options.cwd,
|
|
24481
|
+
env: options.env,
|
|
24482
|
+
signal: controller.signal,
|
|
24483
|
+
maxOutputBytes: 4e3
|
|
24484
|
+
});
|
|
24485
|
+
} finally {
|
|
24486
|
+
clearTimeout(timeout);
|
|
24487
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
24488
|
+
}
|
|
24489
|
+
if (result.aborted) {
|
|
24490
|
+
if (options.signal?.aborted) {
|
|
24491
|
+
throw new Error(`${options.label}.command was aborted`);
|
|
24492
|
+
}
|
|
24493
|
+
throw new Error(
|
|
24494
|
+
`${options.label}.command timed out after ${options.timeoutMs}ms`
|
|
24495
|
+
);
|
|
24496
|
+
}
|
|
24497
|
+
if (result.spawnError) {
|
|
24498
|
+
throw new Error(
|
|
24499
|
+
`${options.label}.command could not start: ${result.spawnError.message}`
|
|
24500
|
+
);
|
|
24501
|
+
}
|
|
24502
|
+
if (result.exitCode !== 0) {
|
|
24503
|
+
const detail = result.stderr.trim();
|
|
24504
|
+
throw new Error(
|
|
24505
|
+
`${options.label}.command exited with code ${result.exitCode ?? "unknown"}${detail ? `: ${detail}` : ""}`
|
|
24506
|
+
);
|
|
24507
|
+
}
|
|
24508
|
+
const output = result.stdout.replace(ANSI_ESCAPE_RE2, "").trim();
|
|
24509
|
+
let parsed;
|
|
24510
|
+
try {
|
|
24511
|
+
parsed = new URL(output);
|
|
24512
|
+
} catch {
|
|
24513
|
+
throw invalidOutputError(options.label);
|
|
24514
|
+
}
|
|
24515
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || output.includes("\n") || output.includes("\r")) {
|
|
24516
|
+
throw invalidOutputError(options.label);
|
|
24517
|
+
}
|
|
24518
|
+
return output;
|
|
24519
|
+
}
|
|
24520
|
+
function invalidOutputError(label) {
|
|
24521
|
+
return new Error(
|
|
24522
|
+
`${label}.command must print exactly one valid HTTP(S) URL to stdout`
|
|
24523
|
+
);
|
|
24524
|
+
}
|
|
24525
|
+
|
|
24394
24526
|
// ../tempo-devserver/app-devserver.ts
|
|
24395
24527
|
var APP_DEVSERVER_STATE_FILENAME = "app-devserver.json";
|
|
24396
24528
|
var APP_DEVSERVER_LOG_FILENAME = "app-devserver.log";
|
|
@@ -24401,8 +24533,8 @@ var DEFAULT_HTTP_READY_TIMEOUT_MS = 18e4;
|
|
|
24401
24533
|
var HTTP_PROBE_ATTEMPT_TIMEOUT_MS = 5e3;
|
|
24402
24534
|
var HTTP_PROBE_ABSOLUTE_CAP_MS = 6e5;
|
|
24403
24535
|
var PUBLIC_HOSTNAME = "127.0.0.1";
|
|
24404
|
-
var PERSISTED_STATE_VERSION =
|
|
24405
|
-
var
|
|
24536
|
+
var PERSISTED_STATE_VERSION = 6;
|
|
24537
|
+
var ANSI_ESCAPE_RE3 = /\x1b\[[0-9;]*m/g;
|
|
24406
24538
|
var URL_RE = /https?:\/\/[^\s"'`<>]+/gi;
|
|
24407
24539
|
var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
24408
24540
|
"127.0.0.1",
|
|
@@ -24421,6 +24553,12 @@ var AppDevServer = class _AppDevServer {
|
|
|
24421
24553
|
error: null
|
|
24422
24554
|
};
|
|
24423
24555
|
this.child = null;
|
|
24556
|
+
this.launcherFinished = false;
|
|
24557
|
+
this.commandEnv = null;
|
|
24558
|
+
this.urlHealthProbeTimer = null;
|
|
24559
|
+
this.urlHealthProbeUrl = null;
|
|
24560
|
+
this.urlHealthProbeConsecutiveFailures = 0;
|
|
24561
|
+
this.urlHealthProbeInFlight = false;
|
|
24424
24562
|
/** Invalidates late stdout/stderr/exit events from an earlier start cycle. */
|
|
24425
24563
|
this.outputEpoch = 0;
|
|
24426
24564
|
this.recentOutput = [];
|
|
@@ -24469,11 +24607,15 @@ var AppDevServer = class _AppDevServer {
|
|
|
24469
24607
|
this.hasParentDeathWatchdog = false;
|
|
24470
24608
|
this.options = {
|
|
24471
24609
|
command: options.command,
|
|
24610
|
+
stop: options.stop,
|
|
24472
24611
|
url: options.url,
|
|
24473
24612
|
cwd: path12.resolve(options.cwd),
|
|
24474
24613
|
workspaceRoot: path12.resolve(options.workspaceRoot),
|
|
24475
24614
|
urlScrapeTimeoutMs: resolveUrlScrapeTimeout(options.urlScrapeTimeoutMs),
|
|
24476
24615
|
httpReadyTimeoutMs: resolveHttpReadyTimeout(options.httpReadyTimeoutMs),
|
|
24616
|
+
urlCommandTimeoutMs: options.urlCommandTimeoutMs ?? 3e4,
|
|
24617
|
+
urlHealthProbeIntervalMs: options.urlHealthProbeIntervalMs ?? 5e3,
|
|
24618
|
+
urlHealthFailuresToDemote: options.urlHealthFailuresToDemote ?? 3,
|
|
24477
24619
|
portRange: options.portRange ?? DEFAULT_PORT_RANGE,
|
|
24478
24620
|
ownership: options.ownership
|
|
24479
24621
|
};
|
|
@@ -24491,6 +24633,7 @@ var AppDevServer = class _AppDevServer {
|
|
|
24491
24633
|
getStatus() {
|
|
24492
24634
|
return {
|
|
24493
24635
|
...this.status,
|
|
24636
|
+
livenessSource: this.status.phase === "running" ? this.launcherFinished ? "probe" : "child" : null,
|
|
24494
24637
|
recentOutputTail: this.recentOutput.join("").slice(-2e3)
|
|
24495
24638
|
};
|
|
24496
24639
|
}
|
|
@@ -24531,6 +24674,11 @@ var AppDevServer = class _AppDevServer {
|
|
|
24531
24674
|
if (ownership !== "reapable") {
|
|
24532
24675
|
return ownership;
|
|
24533
24676
|
}
|
|
24677
|
+
let externalRuntimeReaped = false;
|
|
24678
|
+
if (persisted.externalRuntime) {
|
|
24679
|
+
externalRuntimeReaped = await this.cleanupPersistedExternalRuntime(persisted);
|
|
24680
|
+
if (!externalRuntimeReaped) return "orphan_cleanup_failed";
|
|
24681
|
+
}
|
|
24534
24682
|
const identities = [
|
|
24535
24683
|
[persisted.pid, persisted.pidStartedAt, "shell"],
|
|
24536
24684
|
[persisted.serverPid, persisted.serverPidStartedAt, "server"]
|
|
@@ -24538,7 +24686,9 @@ var AppDevServer = class _AppDevServer {
|
|
|
24538
24686
|
const hadProcessIdentity = identities.some(
|
|
24539
24687
|
([pid, startedAt]) => pid != null && startedAt != null
|
|
24540
24688
|
);
|
|
24541
|
-
if (!hadProcessIdentity)
|
|
24689
|
+
if (!hadProcessIdentity) {
|
|
24690
|
+
return externalRuntimeReaped ? "orphan_reaped" : "nothing_to_reconcile";
|
|
24691
|
+
}
|
|
24542
24692
|
if (persisted.pid != null && await pidStartIdentityMatches(persisted.pid, persisted.pidStartedAt)) {
|
|
24543
24693
|
const outcome = await reapOwnedProcessGroup(persisted.pid);
|
|
24544
24694
|
devLog("app-devserver:registry-orphan-group-reap", {
|
|
@@ -24581,6 +24731,27 @@ var AppDevServer = class _AppDevServer {
|
|
|
24581
24731
|
await this.persistServerPid(null);
|
|
24582
24732
|
return "orphan_reaped";
|
|
24583
24733
|
}
|
|
24734
|
+
async cleanupPersistedExternalRuntime(persisted) {
|
|
24735
|
+
const external = persisted.externalRuntime;
|
|
24736
|
+
if (!external) return true;
|
|
24737
|
+
const result = await this.runBoundedCommand(
|
|
24738
|
+
external.stopCommand,
|
|
24739
|
+
persisted.port,
|
|
24740
|
+
1e4,
|
|
24741
|
+
void 0,
|
|
24742
|
+
external.cwd
|
|
24743
|
+
);
|
|
24744
|
+
devLog("app-devserver:persisted-external-stop", {
|
|
24745
|
+
command: external.stopCommand,
|
|
24746
|
+
exitCode: result.exitCode,
|
|
24747
|
+
aborted: result.aborted,
|
|
24748
|
+
spawnError: result.spawnError?.message ?? null,
|
|
24749
|
+
workspaceRoot: this.options.workspaceRoot
|
|
24750
|
+
});
|
|
24751
|
+
const stopped = external.url ? await waitForAppUrlUnreachable(external.url, 5e3) : describeShellCommandFailure(result) === null;
|
|
24752
|
+
if (stopped) await this.persistExternalRuntime(null);
|
|
24753
|
+
return stopped;
|
|
24754
|
+
}
|
|
24584
24755
|
onStatusChange(listener) {
|
|
24585
24756
|
this.listeners.push(listener);
|
|
24586
24757
|
return () => {
|
|
@@ -24643,6 +24814,7 @@ var AppDevServer = class _AppDevServer {
|
|
|
24643
24814
|
*/
|
|
24644
24815
|
forceKill() {
|
|
24645
24816
|
this.intentionalStop = true;
|
|
24817
|
+
this.stopUrlHealthProbe();
|
|
24646
24818
|
this.outputEpoch += 1;
|
|
24647
24819
|
this.clearOutputBroadcastTimer();
|
|
24648
24820
|
const child = this.child;
|
|
@@ -24726,8 +24898,12 @@ var AppDevServer = class _AppDevServer {
|
|
|
24726
24898
|
const outputEpoch = ++this.outputEpoch;
|
|
24727
24899
|
this.setStatus({ phase: "starting", error: null });
|
|
24728
24900
|
this.intentionalStop = false;
|
|
24901
|
+
this.launcherFinished = false;
|
|
24902
|
+
this.commandEnv = null;
|
|
24903
|
+
this.stopUrlHealthProbe();
|
|
24729
24904
|
this.detectedBaseUrl = null;
|
|
24730
24905
|
this.recentOutput = [];
|
|
24906
|
+
let startSpawned = false;
|
|
24731
24907
|
const dialectIssue = findPowerShellSyntax(this.options.command);
|
|
24732
24908
|
if (dialectIssue) {
|
|
24733
24909
|
const error = `The app dev command was rejected before spawn: it contains PowerShell-only syntax (${dialectIssue}), which the shell Tempo runs commands with (cmd.exe on Windows) cannot parse \u2014 nothing was started. Rewrite "apps[0].start" in tempo.config.json as a single plain cross-platform command \u2014 use \${APP_PORT} only if it needs Tempo's allocated helper port \u2014 and if the app lives in a subdirectory set "appDir" instead of cd/pushd-ing inside the command (chaining with \`&&\` is fine; cmd supports it).`;
|
|
@@ -24738,6 +24914,19 @@ var AppDevServer = class _AppDevServer {
|
|
|
24738
24914
|
this.setStatus({ phase: "error", error, url: null });
|
|
24739
24915
|
return { ok: false, url: null, error };
|
|
24740
24916
|
}
|
|
24917
|
+
const stopDialectIssue = this.options.stop ? findPowerShellSyntax(this.options.stop) : null;
|
|
24918
|
+
if (stopDialectIssue) {
|
|
24919
|
+
const error = `The app stop command was rejected before spawn: it contains PowerShell-only syntax (${stopDialectIssue}). Rewrite "apps[0].stop" as one plain cross-platform command.`;
|
|
24920
|
+
this.setStatus({ phase: "error", error, url: null });
|
|
24921
|
+
return { ok: false, url: null, error };
|
|
24922
|
+
}
|
|
24923
|
+
const urlCommand = typeof this.options.url === "object" ? this.options.url.command : null;
|
|
24924
|
+
const urlDialectIssue = urlCommand ? findPowerShellSyntax(urlCommand) : null;
|
|
24925
|
+
if (urlDialectIssue) {
|
|
24926
|
+
const error = `The app URL command was rejected before spawn: it contains PowerShell-only syntax (${urlDialectIssue}). Rewrite "apps[0].url.command" as one plain cross-platform command.`;
|
|
24927
|
+
this.setStatus({ phase: "error", error, url: null });
|
|
24928
|
+
return { ok: false, url: null, error };
|
|
24929
|
+
}
|
|
24741
24930
|
await this.truncateLogFile();
|
|
24742
24931
|
throwIfAppLaunchAborted(signal);
|
|
24743
24932
|
let removeAbortListener = () => {
|
|
@@ -24760,29 +24949,9 @@ var AppDevServer = class _AppDevServer {
|
|
|
24760
24949
|
this.substitutePort(this.options.command, port)
|
|
24761
24950
|
);
|
|
24762
24951
|
const command = lifted.command;
|
|
24763
|
-
const env =
|
|
24764
|
-
...process.env,
|
|
24765
|
-
APP_PORT: String(port),
|
|
24766
|
-
PORT: String(port),
|
|
24767
|
-
TEMPO_APP_PORT: String(port),
|
|
24768
|
-
// Activate the Tempo annotation plugin (tempoVitePlugin /
|
|
24769
|
-
// tempoNextjsPlugin / Expo babel plugin), which no-ops unless
|
|
24770
|
-
// TEMPO=true. Without it the app server serves route storyboards with
|
|
24771
|
-
// no data-tempo-* source annotations, so their elements can't be
|
|
24772
|
-
// mapped back to source and aren't selectable on the canvas ("route
|
|
24773
|
-
// storyboard elements not clickable"). The Next.js/Expo command shapes
|
|
24774
|
-
// already prefix TEMPO=true, but the Vite shape does not, so the
|
|
24775
|
-
// supervisor owns this for every framework. Same signal the canvas
|
|
24776
|
-
// sidecar sets via buildManagedCommandEnv (TempoHostProcess).
|
|
24777
|
-
TEMPO: "true"
|
|
24778
|
-
};
|
|
24779
|
-
for (const key of Object.keys(env)) {
|
|
24780
|
-
const k2 = key.toLowerCase();
|
|
24781
|
-
if (k2.startsWith("npm_") || k2.startsWith("pnpm_") || key === "INIT_CWD" || key === "NODE_PATH") {
|
|
24782
|
-
delete env[key];
|
|
24783
|
-
}
|
|
24784
|
-
}
|
|
24952
|
+
const env = buildAppCommandEnv(port);
|
|
24785
24953
|
Object.assign(env, lifted.env);
|
|
24954
|
+
this.commandEnv = env;
|
|
24786
24955
|
devLog("app-devserver:spawn", {
|
|
24787
24956
|
command,
|
|
24788
24957
|
configuredUrl: this.options.url ?? null,
|
|
@@ -24817,6 +24986,7 @@ var AppDevServer = class _AppDevServer {
|
|
|
24817
24986
|
windowsHide: true,
|
|
24818
24987
|
env: commandEnv
|
|
24819
24988
|
});
|
|
24989
|
+
startSpawned = true;
|
|
24820
24990
|
this.wireProcessEvents(outputEpoch);
|
|
24821
24991
|
const onAbort = () => {
|
|
24822
24992
|
void this.killChild();
|
|
@@ -24837,19 +25007,39 @@ var AppDevServer = class _AppDevServer {
|
|
|
24837
25007
|
};
|
|
24838
25008
|
return await this.runStartCycle(attempt + 1, signal);
|
|
24839
25009
|
}
|
|
25010
|
+
const startError = this.status.error;
|
|
25011
|
+
const cleanupError2 = await this.cleanupFailedLaunch(
|
|
25012
|
+
port,
|
|
25013
|
+
startSpawned
|
|
25014
|
+
);
|
|
25015
|
+
const finalError2 = cleanupError2 ? `${startError ?? "App dev server failed to start."}
|
|
25016
|
+
|
|
25017
|
+
Cleanup also failed: ${cleanupError2}` : startError;
|
|
25018
|
+
this.setStatus({ phase: "error", error: finalError2, url: null });
|
|
24840
25019
|
return {
|
|
24841
25020
|
ok: false,
|
|
24842
25021
|
url: null,
|
|
24843
|
-
error:
|
|
25022
|
+
error: finalError2
|
|
24844
25023
|
};
|
|
24845
25024
|
}
|
|
24846
25025
|
const error = "App dev server did not become reachable in time.";
|
|
24847
|
-
await this.
|
|
24848
|
-
|
|
24849
|
-
|
|
25026
|
+
const cleanupError = await this.cleanupFailedLaunch(port, startSpawned);
|
|
25027
|
+
const finalError = cleanupError ? `${error}
|
|
25028
|
+
|
|
25029
|
+
Cleanup also failed: ${cleanupError}` : error;
|
|
25030
|
+
this.setStatus({ phase: "error", error: finalError, url: null });
|
|
25031
|
+
return { ok: false, url: null, error: finalError };
|
|
24850
25032
|
}
|
|
24851
25033
|
throwIfAppLaunchAborted(signal);
|
|
24852
25034
|
this.setStatus({ phase: "running", url, error: null });
|
|
25035
|
+
if (this.launcherFinished && this.options.stop) {
|
|
25036
|
+
await this.persistExternalRuntime({
|
|
25037
|
+
stopCommand: this.options.stop,
|
|
25038
|
+
cwd: this.options.cwd,
|
|
25039
|
+
url
|
|
25040
|
+
});
|
|
25041
|
+
}
|
|
25042
|
+
if (this.launcherFinished) this.startUrlHealthProbe(url);
|
|
24853
25043
|
const serverPid = await findPortListenerPid(port);
|
|
24854
25044
|
throwIfAppLaunchAborted(signal);
|
|
24855
25045
|
const persisted = await this.readPersistedState();
|
|
@@ -24867,17 +25057,97 @@ var AppDevServer = class _AppDevServer {
|
|
|
24867
25057
|
return { ok: true, url, error: null };
|
|
24868
25058
|
} catch (error) {
|
|
24869
25059
|
const message = error instanceof Error ? error.message : String(error);
|
|
24870
|
-
await this.
|
|
24871
|
-
|
|
24872
|
-
|
|
25060
|
+
const cleanupError = await this.cleanupFailedLaunch(
|
|
25061
|
+
this.status.port,
|
|
25062
|
+
startSpawned
|
|
25063
|
+
);
|
|
25064
|
+
const finalError = cleanupError ? `${message}
|
|
25065
|
+
|
|
25066
|
+
Cleanup also failed: ${cleanupError}` : message;
|
|
25067
|
+
this.setStatus({ phase: "error", error: finalError });
|
|
25068
|
+
return { ok: false, url: null, error: finalError };
|
|
24873
25069
|
} finally {
|
|
24874
25070
|
removeAbortListener();
|
|
24875
25071
|
}
|
|
24876
25072
|
}
|
|
25073
|
+
/**
|
|
25074
|
+
* Undo any launch attempt that failed after `start` was spawned. External
|
|
25075
|
+
* launchers can leave partial resources even on a nonzero exit, and after a
|
|
25076
|
+
* clean handoff killing the child cannot tear down anything at all; `stop`
|
|
25077
|
+
* is the only remaining authority. If cleanup itself fails, retain that
|
|
25078
|
+
* authority durably so the next stop/start/session can retry it.
|
|
25079
|
+
*/
|
|
25080
|
+
async cleanupFailedLaunch(port, startWasSpawned) {
|
|
25081
|
+
this.stopUrlHealthProbe();
|
|
25082
|
+
if (!startWasSpawned) {
|
|
25083
|
+
this.commandEnv = null;
|
|
25084
|
+
return null;
|
|
25085
|
+
}
|
|
25086
|
+
let cleanupError = null;
|
|
25087
|
+
if (this.options.stop && port != null) {
|
|
25088
|
+
const stopResult = await this.runBoundedCommand(
|
|
25089
|
+
this.options.stop,
|
|
25090
|
+
port,
|
|
25091
|
+
1e4
|
|
25092
|
+
);
|
|
25093
|
+
devLog("app-devserver:failed-launch-stop-command", {
|
|
25094
|
+
command: this.options.stop,
|
|
25095
|
+
exitCode: stopResult.exitCode,
|
|
25096
|
+
aborted: stopResult.aborted,
|
|
25097
|
+
spawnError: stopResult.spawnError?.message ?? null,
|
|
25098
|
+
stderr: stopResult.stderr.slice(-1e3),
|
|
25099
|
+
workspaceRoot: this.options.workspaceRoot
|
|
25100
|
+
});
|
|
25101
|
+
cleanupError = describeShellCommandFailure(stopResult);
|
|
25102
|
+
}
|
|
25103
|
+
await this.killChild();
|
|
25104
|
+
if (cleanupError && this.options.stop) {
|
|
25105
|
+
await this.persistExternalRuntime({
|
|
25106
|
+
stopCommand: this.options.stop,
|
|
25107
|
+
cwd: this.options.cwd,
|
|
25108
|
+
url: null
|
|
25109
|
+
});
|
|
25110
|
+
} else {
|
|
25111
|
+
await this.persistExternalRuntime(null);
|
|
25112
|
+
}
|
|
25113
|
+
this.launcherFinished = false;
|
|
25114
|
+
this.commandEnv = null;
|
|
25115
|
+
return cleanupError;
|
|
25116
|
+
}
|
|
24877
25117
|
async runStopCycle() {
|
|
24878
25118
|
this.intentionalStop = true;
|
|
25119
|
+
this.stopUrlHealthProbe();
|
|
24879
25120
|
this.clearOutputBroadcastTimer();
|
|
24880
|
-
const
|
|
25121
|
+
const persistedBeforeStop = await this.readPersistedState();
|
|
25122
|
+
const persistedExternal = persistedBeforeStop?.externalRuntime ?? null;
|
|
25123
|
+
if (persistedExternal && persistedBeforeStop && await this.classifyPersistedProcessOwnership(persistedBeforeStop) !== "reapable") {
|
|
25124
|
+
const message = "External app server teardown belongs to another live Tempo manager; refusing to run its stop command.";
|
|
25125
|
+
this.setStatus({ phase: "error", url: null, error: message });
|
|
25126
|
+
throw new Error(message);
|
|
25127
|
+
}
|
|
25128
|
+
const port = this.status.port ?? persistedBeforeStop?.port ?? null;
|
|
25129
|
+
const url = this.status.url ?? persistedExternal?.url ?? null;
|
|
25130
|
+
const stopCommand = persistedExternal?.stopCommand ?? this.options.stop;
|
|
25131
|
+
const stopCwd = persistedExternal?.cwd ?? this.options.cwd;
|
|
25132
|
+
let stopCommandError = null;
|
|
25133
|
+
if (stopCommand && port != null) {
|
|
25134
|
+
const stopResult = await this.runBoundedCommand(
|
|
25135
|
+
stopCommand,
|
|
25136
|
+
port,
|
|
25137
|
+
1e4,
|
|
25138
|
+
void 0,
|
|
25139
|
+
stopCwd
|
|
25140
|
+
);
|
|
25141
|
+
devLog("app-devserver:stop-command", {
|
|
25142
|
+
command: stopCommand,
|
|
25143
|
+
exitCode: stopResult.exitCode,
|
|
25144
|
+
aborted: stopResult.aborted,
|
|
25145
|
+
spawnError: stopResult.spawnError?.message ?? null,
|
|
25146
|
+
stderr: stopResult.stderr.slice(-1e3),
|
|
25147
|
+
workspaceRoot: this.options.workspaceRoot
|
|
25148
|
+
});
|
|
25149
|
+
stopCommandError = describeShellCommandFailure(stopResult);
|
|
25150
|
+
}
|
|
24881
25151
|
await this.killChild();
|
|
24882
25152
|
this.outputEpoch += 1;
|
|
24883
25153
|
await this.outputLog.flush();
|
|
@@ -24916,8 +25186,21 @@ var AppDevServer = class _AppDevServer {
|
|
|
24916
25186
|
this.setStatus({ phase: "error", url: null, error: message });
|
|
24917
25187
|
throw new Error(message);
|
|
24918
25188
|
}
|
|
25189
|
+
if (url && !await waitForAppUrlUnreachable(url, 5e3)) {
|
|
25190
|
+
const message = `App dev server is still reachable at ${url} after stop`;
|
|
25191
|
+
this.setStatus({ phase: "error", url: null, error: message });
|
|
25192
|
+
throw new Error(message);
|
|
25193
|
+
}
|
|
25194
|
+
if (persistedExternal && !url && stopCommandError) {
|
|
25195
|
+
const message = `External app server teardown could not be verified: ${stopCommandError}`;
|
|
25196
|
+
this.setStatus({ phase: "error", url: null, error: message });
|
|
25197
|
+
throw new Error(message);
|
|
25198
|
+
}
|
|
24919
25199
|
await this.persistChildPid(null);
|
|
24920
25200
|
await this.persistServerPid(null);
|
|
25201
|
+
await this.persistExternalRuntime(null);
|
|
25202
|
+
this.launcherFinished = false;
|
|
25203
|
+
this.commandEnv = null;
|
|
24921
25204
|
this.setStatus({
|
|
24922
25205
|
phase: "stopped",
|
|
24923
25206
|
url: null,
|
|
@@ -25013,6 +25296,25 @@ var AppDevServer = class _AppDevServer {
|
|
|
25013
25296
|
if (outputEpoch !== this.outputEpoch || this.child !== child) return;
|
|
25014
25297
|
this.child = null;
|
|
25015
25298
|
void this.persistChildPid(null);
|
|
25299
|
+
const cleanLauncherExit = !this.intentionalStop && code === 0 && signal == null;
|
|
25300
|
+
const acceptedLauncherHandoff = cleanLauncherExit && this.options.stop != null && (this.status.phase === "starting" || this.status.phase === "running");
|
|
25301
|
+
if (acceptedLauncherHandoff && this.options.stop) {
|
|
25302
|
+
this.launcherFinished = true;
|
|
25303
|
+
void this.persistExternalRuntime({
|
|
25304
|
+
stopCommand: this.options.stop,
|
|
25305
|
+
cwd: this.options.cwd,
|
|
25306
|
+
url: this.status.phase === "running" ? this.status.url : null
|
|
25307
|
+
});
|
|
25308
|
+
if (this.status.phase === "running" && this.status.url) {
|
|
25309
|
+
this.startUrlHealthProbe(this.status.url);
|
|
25310
|
+
}
|
|
25311
|
+
devLog("app-devserver:launcher-finished", {
|
|
25312
|
+
pid: child.pid ?? null,
|
|
25313
|
+
afterReady: this.status.phase === "running",
|
|
25314
|
+
workspaceRoot: this.options.workspaceRoot
|
|
25315
|
+
});
|
|
25316
|
+
return;
|
|
25317
|
+
}
|
|
25016
25318
|
if (!this.intentionalStop && this.status.phase !== "stopped") {
|
|
25017
25319
|
const conflict = this.status.phase === "running" ? parseDevServerLockConflict(this.recentOutput.join("")) : null;
|
|
25018
25320
|
if (conflict != null) {
|
|
@@ -25025,7 +25327,7 @@ var AppDevServer = class _AppDevServer {
|
|
|
25025
25327
|
}
|
|
25026
25328
|
const tail = this.recentOutput.join("").slice(-500);
|
|
25027
25329
|
const hint = tail ? classifyDevServerOutput(tail) : null;
|
|
25028
|
-
const error = `App dev server exited unexpectedly (code ${code ?? "null"}, signal ${signal ?? "null"}).${tail ? `
|
|
25330
|
+
const error = cleanLauncherExit && this.status.phase === "starting" ? 'The app start command exited successfully before the configured URL became ready. Add a matching non-empty "apps[0].stop" command so Tempo has teardown authority for the external server.' : `App dev server exited unexpectedly (code ${code ?? "null"}, signal ${signal ?? "null"}).${tail ? `
|
|
25029
25331
|
|
|
25030
25332
|
${tail}` : ""}${hint ? `
|
|
25031
25333
|
|
|
@@ -25047,12 +25349,12 @@ Hint: ${hint.hint}` : ""}`;
|
|
|
25047
25349
|
});
|
|
25048
25350
|
}
|
|
25049
25351
|
async waitForReady(port, signal) {
|
|
25050
|
-
const configuredUrl = this.
|
|
25352
|
+
const configuredUrl = await this.resolveConfiguredUrl(port, signal);
|
|
25051
25353
|
if (!configuredUrl) {
|
|
25052
25354
|
const scrapeDeadline = Date.now() + this.options.urlScrapeTimeoutMs;
|
|
25053
25355
|
while (!this.detectedBaseUrl && Date.now() < scrapeDeadline) {
|
|
25054
25356
|
throwIfAppLaunchAborted(signal);
|
|
25055
|
-
if (!this.
|
|
25357
|
+
if (!this.hasLivenessAuthority()) return null;
|
|
25056
25358
|
await sleepForAppLaunch(120, signal);
|
|
25057
25359
|
}
|
|
25058
25360
|
}
|
|
@@ -25066,7 +25368,7 @@ Hint: ${hint.hint}` : ""}`;
|
|
|
25066
25368
|
let lastProgressAt = start;
|
|
25067
25369
|
while (true) {
|
|
25068
25370
|
throwIfAppLaunchAborted(signal);
|
|
25069
|
-
if (!this.
|
|
25371
|
+
if (!this.hasLivenessAuthority()) return null;
|
|
25070
25372
|
const outcome = await probeHttpOnce(
|
|
25071
25373
|
publicUrl,
|
|
25072
25374
|
HTTP_PROBE_ATTEMPT_TIMEOUT_MS,
|
|
@@ -25083,6 +25385,112 @@ Hint: ${hint.hint}` : ""}`;
|
|
|
25083
25385
|
await sleepForAppLaunch(httpProbeInterval(now - start), signal);
|
|
25084
25386
|
}
|
|
25085
25387
|
}
|
|
25388
|
+
hasLivenessAuthority() {
|
|
25389
|
+
return this.launcherFinished || this.child !== null;
|
|
25390
|
+
}
|
|
25391
|
+
async resolveConfiguredUrl(port, signal) {
|
|
25392
|
+
const source = this.options.url;
|
|
25393
|
+
if (!source) return null;
|
|
25394
|
+
if (typeof source === "object" && this.options.stop) {
|
|
25395
|
+
await this.waitForLauncherHandoff(signal);
|
|
25396
|
+
}
|
|
25397
|
+
try {
|
|
25398
|
+
return await resolveHttpUrlSource({
|
|
25399
|
+
source,
|
|
25400
|
+
label: "apps[0].url",
|
|
25401
|
+
cwd: this.options.cwd,
|
|
25402
|
+
env: this.commandEnv ?? buildAppCommandEnv(port),
|
|
25403
|
+
port,
|
|
25404
|
+
timeoutMs: this.options.urlCommandTimeoutMs,
|
|
25405
|
+
signal,
|
|
25406
|
+
substitutePort: substituteAppPort
|
|
25407
|
+
});
|
|
25408
|
+
} catch (error) {
|
|
25409
|
+
throwIfAppLaunchAborted(signal);
|
|
25410
|
+
throw error;
|
|
25411
|
+
}
|
|
25412
|
+
}
|
|
25413
|
+
async waitForLauncherHandoff(signal) {
|
|
25414
|
+
const deadline = Date.now() + HTTP_PROBE_ABSOLUTE_CAP_MS;
|
|
25415
|
+
while (!this.launcherFinished) {
|
|
25416
|
+
throwIfAppLaunchAborted(signal);
|
|
25417
|
+
if (!this.child) {
|
|
25418
|
+
throw new Error(
|
|
25419
|
+
this.status.error ?? "The app start command stopped before URL resolution could run."
|
|
25420
|
+
);
|
|
25421
|
+
}
|
|
25422
|
+
if (Date.now() >= deadline) {
|
|
25423
|
+
throw new Error(
|
|
25424
|
+
"Timed out waiting for the app start command to hand off before running apps[0].url.command"
|
|
25425
|
+
);
|
|
25426
|
+
}
|
|
25427
|
+
await sleepForAppLaunch(120, signal);
|
|
25428
|
+
}
|
|
25429
|
+
}
|
|
25430
|
+
async runBoundedCommand(command, port, timeoutMs, signal, cwd = this.options.cwd) {
|
|
25431
|
+
const controller = new AbortController();
|
|
25432
|
+
const onAbort = () => controller.abort();
|
|
25433
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
25434
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
25435
|
+
try {
|
|
25436
|
+
return await runAbortableShellCommand({
|
|
25437
|
+
command: substituteAppPort(command, port),
|
|
25438
|
+
cwd,
|
|
25439
|
+
env: this.commandEnv ?? buildAppCommandEnv(port),
|
|
25440
|
+
signal: controller.signal,
|
|
25441
|
+
maxOutputBytes: 4e3
|
|
25442
|
+
});
|
|
25443
|
+
} finally {
|
|
25444
|
+
clearTimeout(timeout);
|
|
25445
|
+
signal?.removeEventListener("abort", onAbort);
|
|
25446
|
+
}
|
|
25447
|
+
}
|
|
25448
|
+
startUrlHealthProbe(url) {
|
|
25449
|
+
this.stopUrlHealthProbe();
|
|
25450
|
+
this.urlHealthProbeUrl = url;
|
|
25451
|
+
this.urlHealthProbeTimer = setInterval(() => {
|
|
25452
|
+
void this.runUrlHealthProbeTick();
|
|
25453
|
+
}, this.options.urlHealthProbeIntervalMs);
|
|
25454
|
+
this.urlHealthProbeTimer.unref?.();
|
|
25455
|
+
}
|
|
25456
|
+
stopUrlHealthProbe() {
|
|
25457
|
+
if (this.urlHealthProbeTimer) clearInterval(this.urlHealthProbeTimer);
|
|
25458
|
+
this.urlHealthProbeTimer = null;
|
|
25459
|
+
this.urlHealthProbeUrl = null;
|
|
25460
|
+
this.urlHealthProbeConsecutiveFailures = 0;
|
|
25461
|
+
this.urlHealthProbeInFlight = false;
|
|
25462
|
+
}
|
|
25463
|
+
async runUrlHealthProbeTick() {
|
|
25464
|
+
if (this.urlHealthProbeInFlight) return;
|
|
25465
|
+
const url = this.urlHealthProbeUrl;
|
|
25466
|
+
if (!url || this.intentionalStop) return;
|
|
25467
|
+
this.urlHealthProbeInFlight = true;
|
|
25468
|
+
try {
|
|
25469
|
+
const outcome = await probeHttpOnce(url, 1500);
|
|
25470
|
+
if (this.urlHealthProbeUrl !== url || this.intentionalStop) return;
|
|
25471
|
+
if (outcome === "reachable" || outcome === "held") {
|
|
25472
|
+
this.urlHealthProbeConsecutiveFailures = 0;
|
|
25473
|
+
return;
|
|
25474
|
+
}
|
|
25475
|
+
if (!this.launcherFinished) return;
|
|
25476
|
+
this.urlHealthProbeConsecutiveFailures += 1;
|
|
25477
|
+
if (this.urlHealthProbeConsecutiveFailures < this.options.urlHealthFailuresToDemote) {
|
|
25478
|
+
return;
|
|
25479
|
+
}
|
|
25480
|
+
const error = `App dev server URL became unreachable after launcher handoff: ${url}`;
|
|
25481
|
+
this.launcherFinished = false;
|
|
25482
|
+
this.stopUrlHealthProbe();
|
|
25483
|
+
this.setStatus({ phase: "error", url: null, error });
|
|
25484
|
+
for (const listener of [...this.exitListeners]) {
|
|
25485
|
+
try {
|
|
25486
|
+
listener({ code: null, signal: null, error });
|
|
25487
|
+
} catch {
|
|
25488
|
+
}
|
|
25489
|
+
}
|
|
25490
|
+
} finally {
|
|
25491
|
+
this.urlHealthProbeInFlight = false;
|
|
25492
|
+
}
|
|
25493
|
+
}
|
|
25086
25494
|
async killChild() {
|
|
25087
25495
|
const child = this.child;
|
|
25088
25496
|
if (!child) return;
|
|
@@ -25172,6 +25580,19 @@ Hint: ${hint.hint}` : ""}`;
|
|
|
25172
25580
|
async allocatePort() {
|
|
25173
25581
|
const persisted = await this.readPersistedState();
|
|
25174
25582
|
if (persisted) {
|
|
25583
|
+
if (persisted.externalRuntime) {
|
|
25584
|
+
const ownership = await this.classifyPersistedProcessOwnership(persisted);
|
|
25585
|
+
if (ownership !== "reapable") {
|
|
25586
|
+
throw new Error(
|
|
25587
|
+
"An external app server is still owned by another Tempo manager; refusing to start a duplicate."
|
|
25588
|
+
);
|
|
25589
|
+
}
|
|
25590
|
+
if (!await this.cleanupPersistedExternalRuntime(persisted)) {
|
|
25591
|
+
throw new Error(
|
|
25592
|
+
"The previously launched external app server could not be stopped; refusing to start a duplicate."
|
|
25593
|
+
);
|
|
25594
|
+
}
|
|
25595
|
+
}
|
|
25175
25596
|
if (await isPortFree(persisted.port)) {
|
|
25176
25597
|
return persisted.port;
|
|
25177
25598
|
}
|
|
@@ -25227,6 +25648,7 @@ Hint: ${hint.hint}` : ""}`;
|
|
|
25227
25648
|
pidStartedAt: null,
|
|
25228
25649
|
serverPid: null,
|
|
25229
25650
|
serverPidStartedAt: null,
|
|
25651
|
+
externalRuntime: null,
|
|
25230
25652
|
...this.persistedOwnership()
|
|
25231
25653
|
});
|
|
25232
25654
|
return fresh;
|
|
@@ -25257,6 +25679,7 @@ Hint: ${hint.hint}` : ""}`;
|
|
|
25257
25679
|
pidStartedAt,
|
|
25258
25680
|
serverPid: persisted.serverPid,
|
|
25259
25681
|
serverPidStartedAt: persisted.serverPidStartedAt,
|
|
25682
|
+
externalRuntime: persisted.externalRuntime,
|
|
25260
25683
|
...this.persistedOwnership(persisted)
|
|
25261
25684
|
});
|
|
25262
25685
|
});
|
|
@@ -25286,6 +25709,19 @@ Hint: ${hint.hint}` : ""}`;
|
|
|
25286
25709
|
pidStartedAt: persisted.pidStartedAt,
|
|
25287
25710
|
serverPid,
|
|
25288
25711
|
serverPidStartedAt,
|
|
25712
|
+
externalRuntime: persisted.externalRuntime,
|
|
25713
|
+
...this.persistedOwnership(persisted)
|
|
25714
|
+
});
|
|
25715
|
+
});
|
|
25716
|
+
}
|
|
25717
|
+
async persistExternalRuntime(externalRuntime) {
|
|
25718
|
+
await this.runExclusiveStateFileOp(async () => {
|
|
25719
|
+
const persisted = await this.readPersistedState();
|
|
25720
|
+
if (!persisted) return;
|
|
25721
|
+
await this.writePersistedStateUnlocked({
|
|
25722
|
+
...persisted,
|
|
25723
|
+
version: PERSISTED_STATE_VERSION,
|
|
25724
|
+
externalRuntime,
|
|
25289
25725
|
...this.persistedOwnership(persisted)
|
|
25290
25726
|
});
|
|
25291
25727
|
});
|
|
@@ -25341,7 +25777,12 @@ Hint: ${hint.hint}` : ""}`;
|
|
|
25341
25777
|
serverPidStartedAt: typeof parsed.serverPidStartedAt === "string" ? parsed.serverPidStartedAt : null,
|
|
25342
25778
|
ownerId: typeof parsed.ownerId === "string" ? parsed.ownerId : null,
|
|
25343
25779
|
managerPid: typeof parsed.managerPid === "number" && Number.isInteger(parsed.managerPid) ? parsed.managerPid : null,
|
|
25344
|
-
managerStartedAt: typeof parsed.managerStartedAt === "string" ? parsed.managerStartedAt : null
|
|
25780
|
+
managerStartedAt: typeof parsed.managerStartedAt === "string" ? parsed.managerStartedAt : null,
|
|
25781
|
+
externalRuntime: parsed.externalRuntime && typeof parsed.externalRuntime === "object" && typeof parsed.externalRuntime.stopCommand === "string" && typeof parsed.externalRuntime.cwd === "string" && (typeof parsed.externalRuntime.url === "string" || parsed.externalRuntime.url === null) ? {
|
|
25782
|
+
stopCommand: parsed.externalRuntime.stopCommand,
|
|
25783
|
+
cwd: parsed.externalRuntime.cwd,
|
|
25784
|
+
url: parsed.externalRuntime.url
|
|
25785
|
+
} : null
|
|
25345
25786
|
};
|
|
25346
25787
|
}
|
|
25347
25788
|
} catch {
|
|
@@ -25414,7 +25855,7 @@ Hint: ${hint.hint}` : ""}`;
|
|
|
25414
25855
|
}
|
|
25415
25856
|
};
|
|
25416
25857
|
function extractServerUrl(text) {
|
|
25417
|
-
const stripped = text.replace(
|
|
25858
|
+
const stripped = text.replace(ANSI_ESCAPE_RE3, "");
|
|
25418
25859
|
const matches = stripped.match(URL_RE);
|
|
25419
25860
|
if (!matches) return null;
|
|
25420
25861
|
for (const raw of matches) {
|
|
@@ -25532,6 +25973,39 @@ function waitForWatchdogChildPid(watchdog, timeoutMs = 2e3) {
|
|
|
25532
25973
|
function substituteAppPort(command, port) {
|
|
25533
25974
|
return command.replace(/\$\{(?:APP_PORT|PORT)\}/g, String(port));
|
|
25534
25975
|
}
|
|
25976
|
+
function buildAppCommandEnv(port) {
|
|
25977
|
+
const env = {
|
|
25978
|
+
...process.env,
|
|
25979
|
+
APP_PORT: String(port),
|
|
25980
|
+
PORT: String(port),
|
|
25981
|
+
TEMPO_APP_PORT: String(port),
|
|
25982
|
+
TEMPO: "true"
|
|
25983
|
+
};
|
|
25984
|
+
for (const key of Object.keys(env)) {
|
|
25985
|
+
const normalized = key.toLowerCase();
|
|
25986
|
+
if (normalized.startsWith("npm_") || normalized.startsWith("pnpm_") || key === "INIT_CWD" || key === "NODE_PATH") {
|
|
25987
|
+
delete env[key];
|
|
25988
|
+
}
|
|
25989
|
+
}
|
|
25990
|
+
return env;
|
|
25991
|
+
}
|
|
25992
|
+
async function waitForAppUrlUnreachable(url, timeoutMs) {
|
|
25993
|
+
const startedAt = Date.now();
|
|
25994
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
25995
|
+
if (await probeHttpOnce(url, 750) === "refused") return true;
|
|
25996
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
25997
|
+
}
|
|
25998
|
+
return await probeHttpOnce(url, 750) === "refused";
|
|
25999
|
+
}
|
|
26000
|
+
function describeShellCommandFailure(result) {
|
|
26001
|
+
if (result.aborted) return "the app stop command timed out";
|
|
26002
|
+
if (result.spawnError) {
|
|
26003
|
+
return `the app stop command could not start: ${result.spawnError.message}`;
|
|
26004
|
+
}
|
|
26005
|
+
if (result.exitCode === 0) return null;
|
|
26006
|
+
const detail = result.stderr.trim();
|
|
26007
|
+
return `the app stop command exited with code ${result.exitCode ?? "unknown"}${detail ? `: ${detail}` : ""}`;
|
|
26008
|
+
}
|
|
25535
26009
|
async function findFreePort(range) {
|
|
25536
26010
|
const maxAttempts = 50;
|
|
25537
26011
|
for (let i = 0; i < maxAttempts; i++) {
|
|
@@ -26343,7 +26817,7 @@ async function parcelEventToFileEvent(event, rootInfo) {
|
|
|
26343
26817
|
// ../tempo-sdk/package.json
|
|
26344
26818
|
var package_default = {
|
|
26345
26819
|
name: "tempo-sdk",
|
|
26346
|
-
version: "0.0.
|
|
26820
|
+
version: "0.0.34",
|
|
26347
26821
|
type: "module",
|
|
26348
26822
|
description: "Tempo SDK \u2014 Vite, Next.js, and Expo plugins for JSX annotation and shared page/storyboard types",
|
|
26349
26823
|
repository: {
|
|
@@ -28062,6 +28536,9 @@ var TempoHostProcess = class _TempoHostProcess {
|
|
|
28062
28536
|
this.lastHint = null;
|
|
28063
28537
|
this.hintListeners = [];
|
|
28064
28538
|
this.explicitTempoHostUrl = options.explicitTempoHostUrl;
|
|
28539
|
+
this.tempoHostUrlSource = options.tempoHostUrlSource;
|
|
28540
|
+
this.injectedPort = options.injectedPort;
|
|
28541
|
+
this.launchSignal = options.launchSignal;
|
|
28065
28542
|
this.tempoHostUrlIsHardcoded = options.tempoHostUrlIsHardcoded ?? false;
|
|
28066
28543
|
this.expectedProjectRoot = options.expectedProjectRoot;
|
|
28067
28544
|
this.expectedIdentity = options.expectedIdentity ?? null;
|
|
@@ -28069,6 +28546,7 @@ var TempoHostProcess = class _TempoHostProcess {
|
|
|
28069
28546
|
this.missingRootDepsProjectRoot = options.missingRootDepsProjectRoot ?? null;
|
|
28070
28547
|
this.commandCwd = options.cwd;
|
|
28071
28548
|
this.stopCommand = options.stopCommand ?? null;
|
|
28549
|
+
this.onLauncherHandoff = options.onLauncherHandoff ?? null;
|
|
28072
28550
|
this.hasParentDeathWatchdog = options.watchdogScriptPath != null;
|
|
28073
28551
|
const lifted = extractLeadingEnvAssignments(options.command);
|
|
28074
28552
|
devLog("process:spawn", {
|
|
@@ -28169,10 +28647,11 @@ ${output}` : "";
|
|
|
28169
28647
|
this.rejectReady = null;
|
|
28170
28648
|
}
|
|
28171
28649
|
});
|
|
28172
|
-
this.child.on("exit", (code, signal) => {
|
|
28650
|
+
this.child.on("exit", async (code, signal) => {
|
|
28173
28651
|
this.stopped = true;
|
|
28174
28652
|
const cleanLauncherExit = !this.intentionalStop && !this.readinessConfirmed && code === 0 && signal == null;
|
|
28175
28653
|
if (cleanLauncherExit && this.stopCommand) {
|
|
28654
|
+
await this.onLauncherHandoff?.();
|
|
28176
28655
|
this.launcherFinished = true;
|
|
28177
28656
|
this.lastExitCrashed = false;
|
|
28178
28657
|
devLog("process:launcher_finished", {
|
|
@@ -28340,7 +28819,21 @@ Hint: ${this.lastHint.hint}` : "";
|
|
|
28340
28819
|
};
|
|
28341
28820
|
}
|
|
28342
28821
|
try {
|
|
28343
|
-
if (
|
|
28822
|
+
if (typeof this.tempoHostUrlSource === "object" && this.tempoHostUrlSource !== null) {
|
|
28823
|
+
if (this.stopCommand) {
|
|
28824
|
+
await this.waitForLauncherHandoff(timeoutMs);
|
|
28825
|
+
}
|
|
28826
|
+
this.explicitTempoHostUrl = await resolveHttpUrlSource({
|
|
28827
|
+
source: this.tempoHostUrlSource,
|
|
28828
|
+
label: "urls.tempoHost",
|
|
28829
|
+
cwd: this.commandCwd,
|
|
28830
|
+
env: this.commandEnv,
|
|
28831
|
+
port: this.injectedPort,
|
|
28832
|
+
timeoutMs: 3e4,
|
|
28833
|
+
signal: this.launchSignal,
|
|
28834
|
+
substitutePort: substituteAppPort
|
|
28835
|
+
});
|
|
28836
|
+
} else if (!this.tempoHostUrlIsHardcoded) {
|
|
28344
28837
|
await withTimeout(
|
|
28345
28838
|
this.readySignal,
|
|
28346
28839
|
timeoutMs,
|
|
@@ -28348,6 +28841,7 @@ Hint: ${this.lastHint.hint}` : "";
|
|
|
28348
28841
|
);
|
|
28349
28842
|
}
|
|
28350
28843
|
} catch (error) {
|
|
28844
|
+
throwIfLaunchAborted(this.launchSignal);
|
|
28351
28845
|
const contractError = error instanceof DaemonizedStartRequiresStopError;
|
|
28352
28846
|
const crashed = this.lastExitCrashed;
|
|
28353
28847
|
return {
|
|
@@ -28446,6 +28940,25 @@ Hint: ${this.lastHint.hint}` : "";
|
|
|
28446
28940
|
pid: this.child.pid ?? null
|
|
28447
28941
|
};
|
|
28448
28942
|
}
|
|
28943
|
+
async waitForLauncherHandoff(timeoutMs) {
|
|
28944
|
+
const deadline = Date.now() + Math.max(timeoutMs, 3e4);
|
|
28945
|
+
while (!this.launcherFinished) {
|
|
28946
|
+
throwIfLaunchAborted(this.launchSignal);
|
|
28947
|
+
if (this.stopped) {
|
|
28948
|
+
await this.readySignal;
|
|
28949
|
+
if (this.launcherFinished) return;
|
|
28950
|
+
throw new Error(
|
|
28951
|
+
"The canvas start command stopped before urls.tempoHost.command could run."
|
|
28952
|
+
);
|
|
28953
|
+
}
|
|
28954
|
+
if (Date.now() >= deadline) {
|
|
28955
|
+
throw new Error(
|
|
28956
|
+
"Timed out waiting for the canvas start command to hand off before running urls.tempoHost.command"
|
|
28957
|
+
);
|
|
28958
|
+
}
|
|
28959
|
+
await waitForLaunchDelay(120, this.launchSignal);
|
|
28960
|
+
}
|
|
28961
|
+
}
|
|
28449
28962
|
async terminate() {
|
|
28450
28963
|
this.intentionalStop = true;
|
|
28451
28964
|
if (this.terminationPromise) return await this.terminationPromise;
|
|
@@ -28545,6 +29058,7 @@ function parsePersistedDevserverOwner(value) {
|
|
|
28545
29058
|
portWasTempoAllocated: record.portWasTempoAllocated === true,
|
|
28546
29059
|
launchPid: typeof record.launchPid === "number" && Number.isInteger(record.launchPid) && record.launchPid > 1 ? record.launchPid : null,
|
|
28547
29060
|
launchProcessStartedAt: typeof record.launchProcessStartedAt === "string" ? record.launchProcessStartedAt : null,
|
|
29061
|
+
launchPort: typeof record.launchPort === "number" && Number.isInteger(record.launchPort) && record.launchPort > 0 && record.launchPort < 65536 ? record.launchPort : null,
|
|
28548
29062
|
stopCommand: typeof record.stopCommand === "string" && record.stopCommand.length > 0 ? record.stopCommand : null
|
|
28549
29063
|
};
|
|
28550
29064
|
}
|
|
@@ -28583,8 +29097,11 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
28583
29097
|
this.devserverReadyAt = null;
|
|
28584
29098
|
this.devserverLaunchPid = null;
|
|
28585
29099
|
this.devserverLaunchProcessStartedAt = null;
|
|
29100
|
+
this.devserverLaunchPort = null;
|
|
28586
29101
|
this.devserverStopCommand = null;
|
|
28587
29102
|
this.devserverPortWasTempoAllocated = false;
|
|
29103
|
+
/** Stop authority persisted after handoff but before URL readiness. */
|
|
29104
|
+
this.devserverExternalRuntimePending = false;
|
|
28588
29105
|
// File watcher on managed files (Section 11.6). On any relevant change/delete
|
|
28589
29106
|
// a debounced repair() call fires. `@modules/file-watcher` wraps
|
|
28590
29107
|
// @parcel/watcher with recursive watching, batching, coalescing, and
|
|
@@ -28875,6 +29392,15 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
28875
29392
|
describePowerShellRejection(startDialectIssue)
|
|
28876
29393
|
);
|
|
28877
29394
|
}
|
|
29395
|
+
const tempoHostUrlCommand = this.config.tempoHostUrl !== null && typeof this.config.tempoHostUrl === "object" ? this.config.tempoHostUrl.command : null;
|
|
29396
|
+
const tempoHostUrlDialectIssue = tempoHostUrlCommand ? findPowerShellSyntax(tempoHostUrlCommand) : null;
|
|
29397
|
+
if (tempoHostUrlCommand && tempoHostUrlDialectIssue) {
|
|
29398
|
+
t.done({ ok: false, reason: "tempo_host_url_shell_dialect_mismatch" });
|
|
29399
|
+
throw new StartShellDialectMismatchError(
|
|
29400
|
+
tempoHostUrlCommand,
|
|
29401
|
+
`urls.tempoHost.command contains PowerShell-only syntax (${tempoHostUrlDialectIssue}); use one plain cross-platform command`
|
|
29402
|
+
);
|
|
29403
|
+
}
|
|
28878
29404
|
this.idleProcessStatus = "starting";
|
|
28879
29405
|
let process2;
|
|
28880
29406
|
let missingRootDepsProjectRoot = null;
|
|
@@ -28912,6 +29438,7 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
28912
29438
|
if (process2 !== activeProcess) {
|
|
28913
29439
|
return;
|
|
28914
29440
|
}
|
|
29441
|
+
this.devserverExternalRuntimePending = false;
|
|
28915
29442
|
this.state.devserverUrl = null;
|
|
28916
29443
|
this.state.urlUnreachableSince = null;
|
|
28917
29444
|
this.idleProcessStatus = "exited";
|
|
@@ -28937,11 +29464,19 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
28937
29464
|
throwIfLaunchAborted(options?.signal);
|
|
28938
29465
|
const expectedIdentity = await this.readExpectedHostIdentity();
|
|
28939
29466
|
const stopCommand = this.config.scripts.stop ? substituteAppPort(this.config.scripts.stop, startPlan.injectedPort) : null;
|
|
28940
|
-
|
|
29467
|
+
this.devserverStopCommand = stopCommand;
|
|
29468
|
+
this.devserverLaunchPort = startPlan.injectedPort;
|
|
29469
|
+
this.devserverPortWasTempoAllocated = startPlan.portWasTempoAllocated;
|
|
29470
|
+
this.devserverExternalRuntimePending = false;
|
|
29471
|
+
let activeProcess;
|
|
29472
|
+
activeProcess = new TempoHostProcess({
|
|
28941
29473
|
cwd: this.commandCwd,
|
|
28942
29474
|
command: startPlan.command,
|
|
28943
29475
|
explicitTempoHostUrl: startPlan.tempoHostUrl,
|
|
28944
|
-
|
|
29476
|
+
tempoHostUrlSource: this.config.tempoHostUrl,
|
|
29477
|
+
injectedPort: startPlan.injectedPort,
|
|
29478
|
+
launchSignal: options?.signal,
|
|
29479
|
+
tempoHostUrlIsHardcoded: typeof this.config.tempoHostUrl === "string" && !this.config.tempoHostUrl.includes("${PORT}"),
|
|
28945
29480
|
expectedProjectRoot: this.projectRoot,
|
|
28946
29481
|
expectedIdentity,
|
|
28947
29482
|
identityFailureMessage: sdkUpgradeResult ? new CanvasHostIdentityError({
|
|
@@ -28953,6 +29488,11 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
28953
29488
|
}).message : null,
|
|
28954
29489
|
missingRootDepsProjectRoot,
|
|
28955
29490
|
stopCommand,
|
|
29491
|
+
onLauncherHandoff: async () => {
|
|
29492
|
+
if (this.activeHostProcess !== activeProcess) return;
|
|
29493
|
+
this.devserverExternalRuntimePending = true;
|
|
29494
|
+
await this.persistStatus();
|
|
29495
|
+
},
|
|
28956
29496
|
// The tempo-sdk Next plugin applies this documented Next config field.
|
|
28957
29497
|
// It keeps Tempo's host cache isolated from the user's own `next dev`.
|
|
28958
29498
|
extraEnv: {
|
|
@@ -28968,8 +29508,6 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
28968
29508
|
this.activeHostProcess = activeProcess;
|
|
28969
29509
|
this.devserverLaunchPid = activeProcess.getPid();
|
|
28970
29510
|
this.devserverLaunchProcessStartedAt = this.devserverLaunchPid ? await readProcessStartedAt(this.devserverLaunchPid) : null;
|
|
28971
|
-
this.devserverStopCommand = stopCommand;
|
|
28972
|
-
this.devserverPortWasTempoAllocated = startPlan.portWasTempoAllocated;
|
|
28973
29511
|
attachProcessListeners(activeProcess);
|
|
28974
29512
|
await this.writeProcessMarker(activeProcess.getPid()).catch(() => {
|
|
28975
29513
|
});
|
|
@@ -29151,6 +29689,7 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
29151
29689
|
self.state.devserverUrl = result.url;
|
|
29152
29690
|
self.state.urlUnreachableSince = null;
|
|
29153
29691
|
self.devserverReadyAt = Date.now();
|
|
29692
|
+
self.devserverExternalRuntimePending = false;
|
|
29154
29693
|
const shellPid = process2.getPid();
|
|
29155
29694
|
if (shellPid) {
|
|
29156
29695
|
const workerPids = new Set(
|
|
@@ -30414,7 +30953,12 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
30414
30953
|
// branch `state.devserverUrl` in THIS instance is left untouched: a
|
|
30415
30954
|
// persisted URL is never resurrected into live state.
|
|
30416
30955
|
async reconcilePersistedDevserverUrl(staleUrl, staleOwner) {
|
|
30417
|
-
if (!staleUrl)
|
|
30956
|
+
if (!staleUrl) {
|
|
30957
|
+
if (!staleOwner) return "nothing_to_reconcile";
|
|
30958
|
+
if (isPidAlive(staleOwner.managerPid)) return "live_sibling";
|
|
30959
|
+
const markedOurs2 = supervisorAppInstanceId !== null && staleOwner.appInstanceId === supervisorAppInstanceId;
|
|
30960
|
+
return markedOurs2 ? await this.purgeOwnedOrphanDevserver(null, staleOwner) : "foreign_process";
|
|
30961
|
+
}
|
|
30418
30962
|
if (staleUrl === this.state.devserverUrl) return "current_runtime";
|
|
30419
30963
|
const markedOurs = staleOwner !== null && supervisorAppInstanceId !== null && staleOwner.appInstanceId === supervisorAppInstanceId;
|
|
30420
30964
|
if (markedOurs) {
|
|
@@ -30455,8 +30999,9 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
30455
30999
|
// listener is signalled only when its pid and birth identity match the
|
|
30456
31000
|
// launch record; a fresh port lookup alone is diagnostic, never proof.
|
|
30457
31001
|
async purgeOwnedOrphanDevserver(staleUrl, owner) {
|
|
30458
|
-
const port = portFromReadyUrl(staleUrl);
|
|
31002
|
+
const port = owner.launchPort ?? (staleUrl ? portFromReadyUrl(staleUrl) : null);
|
|
30459
31003
|
let stopCommandRan = false;
|
|
31004
|
+
let stopCommandSucceeded = false;
|
|
30460
31005
|
if (owner.stopCommand) {
|
|
30461
31006
|
const stopResult = await runBoundedShellCommand({
|
|
30462
31007
|
command: owner.stopCommand,
|
|
@@ -30468,6 +31013,7 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
30468
31013
|
timeoutMs: 1e4
|
|
30469
31014
|
});
|
|
30470
31015
|
stopCommandRan = true;
|
|
31016
|
+
stopCommandSucceeded = stopResult.ok;
|
|
30471
31017
|
devLog("supervisor:owned_orphan_stop_command", {
|
|
30472
31018
|
url: staleUrl,
|
|
30473
31019
|
command: owner.stopCommand,
|
|
@@ -30484,14 +31030,14 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
30484
31030
|
if (maySignalListener && listenerPid !== null) {
|
|
30485
31031
|
await terminateProcessTreeByPid(listenerPid, 3e3, 2e3);
|
|
30486
31032
|
}
|
|
30487
|
-
const finalOutcome = await probeTempoHost(
|
|
31033
|
+
const finalOutcome = staleUrl ? await probeTempoHost(
|
|
30488
31034
|
staleUrl,
|
|
30489
31035
|
1500,
|
|
30490
31036
|
this.projectRoot,
|
|
30491
31037
|
await this.readExpectedHostIdentity()
|
|
30492
|
-
);
|
|
31038
|
+
) : null;
|
|
30493
31039
|
const launchIdentityStillAlive = owner.launchPid !== null && owner.launchProcessStartedAt !== null && await readProcessStartedAt(owner.launchPid) === owner.launchProcessStartedAt;
|
|
30494
|
-
const reconciled = finalOutcome !== "ok"
|
|
31040
|
+
const reconciled = !launchIdentityStillAlive && (staleUrl ? finalOutcome !== "ok" : stopCommandSucceeded);
|
|
30495
31041
|
devLog("supervisor:owned_orphan_purge_result", {
|
|
30496
31042
|
url: staleUrl,
|
|
30497
31043
|
markerPid: owner.managerPid,
|
|
@@ -30514,6 +31060,8 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
30514
31060
|
};
|
|
30515
31061
|
if (this.state.devserverUrl) {
|
|
30516
31062
|
nextStatus.devserverUrl = this.state.devserverUrl;
|
|
31063
|
+
}
|
|
31064
|
+
if (this.state.devserverUrl || this.devserverExternalRuntimePending) {
|
|
30517
31065
|
nextStatus.devserverOwner = {
|
|
30518
31066
|
appInstanceId: supervisorAppInstanceId,
|
|
30519
31067
|
managerPid: process.pid,
|
|
@@ -30521,6 +31069,7 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
30521
31069
|
portWasTempoAllocated: this.devserverPortWasTempoAllocated,
|
|
30522
31070
|
launchPid: this.devserverLaunchPid,
|
|
30523
31071
|
launchProcessStartedAt: this.devserverLaunchProcessStartedAt,
|
|
31072
|
+
launchPort: this.devserverLaunchPort,
|
|
30524
31073
|
stopCommand: this.devserverStopCommand
|
|
30525
31074
|
};
|
|
30526
31075
|
}
|
|
@@ -30957,7 +31506,8 @@ async function getAvailableLocalPort() {
|
|
|
30957
31506
|
});
|
|
30958
31507
|
}
|
|
30959
31508
|
function portFromConfiguredTempoHostUrl(tempoHostUrl) {
|
|
30960
|
-
if (!tempoHostUrl || tempoHostUrl.includes("${PORT}"))
|
|
31509
|
+
if (!tempoHostUrl || typeof tempoHostUrl !== "string" || tempoHostUrl.includes("${PORT}"))
|
|
31510
|
+
return null;
|
|
30961
31511
|
try {
|
|
30962
31512
|
const parsed = new URL(tempoHostUrl);
|
|
30963
31513
|
if (parsed.port) return Number(parsed.port);
|
|
@@ -30968,10 +31518,10 @@ function portFromConfiguredTempoHostUrl(tempoHostUrl) {
|
|
|
30968
31518
|
function buildManagedStartPlan(options) {
|
|
30969
31519
|
const commandUsesPortToken = options.command.includes("${PORT}");
|
|
30970
31520
|
const configuredPort = portFromConfiguredTempoHostUrl(options.tempoHostUrl);
|
|
30971
|
-
const urlUsesPortToken = options.tempoHostUrl
|
|
31521
|
+
const urlUsesPortToken = typeof options.tempoHostUrl === "string" && options.tempoHostUrl.includes("${PORT}");
|
|
30972
31522
|
const recognizedCommand = canInjectDevServerPort(options.command);
|
|
30973
31523
|
const command = commandUsesPortToken ? substituteAppPort(options.command, options.port) : recognizedCommand ? injectDevServerPort(options.command, options.port, options.framework) : options.command;
|
|
30974
|
-
const tempoHostUrl = options.tempoHostUrl ? substituteAppPort(options.tempoHostUrl, options.port) : null;
|
|
31524
|
+
const tempoHostUrl = typeof options.tempoHostUrl === "string" ? substituteAppPort(options.tempoHostUrl, options.port) : null;
|
|
30975
31525
|
const shouldExportGenericPort = commandUsesPortToken || urlUsesPortToken || recognizedCommand || options.tempoHostUrl !== null;
|
|
30976
31526
|
return {
|
|
30977
31527
|
command,
|
|
@@ -32319,13 +32869,7 @@ function validateAppStart(value, label = "scripts.appStart") {
|
|
|
32319
32869
|
const trimmed2 = value.trim();
|
|
32320
32870
|
return trimmed2;
|
|
32321
32871
|
}
|
|
32322
|
-
function
|
|
32323
|
-
if (value === void 0 || value === null) return void 0;
|
|
32324
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
32325
|
-
throw new Error(
|
|
32326
|
-
`tempo.config.json field "${label}" must be a non-empty string when set`
|
|
32327
|
-
);
|
|
32328
|
-
}
|
|
32872
|
+
function validateHttpUrlString(value, label) {
|
|
32329
32873
|
const trimmed2 = value.trim();
|
|
32330
32874
|
let parsed;
|
|
32331
32875
|
try {
|
|
@@ -32340,6 +32884,29 @@ function validateAppUrl(value, label) {
|
|
|
32340
32884
|
}
|
|
32341
32885
|
return trimmed2;
|
|
32342
32886
|
}
|
|
32887
|
+
function validateHttpUrlSource(value, label) {
|
|
32888
|
+
if (value === void 0 || value === null) return void 0;
|
|
32889
|
+
if (typeof value === "string") {
|
|
32890
|
+
if (value.trim().length === 0) {
|
|
32891
|
+
throw new Error(
|
|
32892
|
+
`tempo.config.json field "${label}" must be a non-empty string when set`
|
|
32893
|
+
);
|
|
32894
|
+
}
|
|
32895
|
+
return validateHttpUrlString(value, label);
|
|
32896
|
+
}
|
|
32897
|
+
if (!isObjectRecord(value)) {
|
|
32898
|
+
throw new Error(
|
|
32899
|
+
`tempo.config.json field "${label}" must be an HTTP(S) URL string or an object with a non-empty "command"`
|
|
32900
|
+
);
|
|
32901
|
+
}
|
|
32902
|
+
const keys = Object.keys(value);
|
|
32903
|
+
if (keys.length !== 1 || keys[0] !== "command" || typeof value.command !== "string" || value.command.trim().length === 0) {
|
|
32904
|
+
throw new Error(
|
|
32905
|
+
`tempo.config.json field "${label}" command source must be exactly { "command": "<non-empty command>" }`
|
|
32906
|
+
);
|
|
32907
|
+
}
|
|
32908
|
+
return { command: value.command.trim() };
|
|
32909
|
+
}
|
|
32343
32910
|
function validateScriptsStop(value) {
|
|
32344
32911
|
if (value === void 0 || value === null) return void 0;
|
|
32345
32912
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
@@ -32356,20 +32923,11 @@ function validateUrls(value) {
|
|
|
32356
32923
|
}
|
|
32357
32924
|
const raw = value;
|
|
32358
32925
|
if (raw.tempoHost !== void 0) {
|
|
32359
|
-
|
|
32360
|
-
|
|
32361
|
-
|
|
32362
|
-
|
|
32363
|
-
}
|
|
32364
|
-
const tempoHost = raw.tempoHost.trim();
|
|
32365
|
-
try {
|
|
32366
|
-
new URL(tempoHost.replace(/\$\{PORT\}/g, "5173"));
|
|
32367
|
-
} catch {
|
|
32368
|
-
throw new Error(
|
|
32369
|
-
'tempo.config.json field "urls.tempoHost" must be a valid URL'
|
|
32370
|
-
);
|
|
32371
|
-
}
|
|
32372
|
-
return { tempoHost };
|
|
32926
|
+
const tempoHost = validateHttpUrlSource(
|
|
32927
|
+
raw.tempoHost,
|
|
32928
|
+
"urls.tempoHost"
|
|
32929
|
+
);
|
|
32930
|
+
return tempoHost === void 0 ? void 0 : { tempoHost };
|
|
32373
32931
|
}
|
|
32374
32932
|
return void 0;
|
|
32375
32933
|
}
|
|
@@ -32444,8 +33002,9 @@ function validateApps(value) {
|
|
|
32444
33002
|
}
|
|
32445
33003
|
seenAppDirs.add(appDir);
|
|
32446
33004
|
}
|
|
32447
|
-
const url =
|
|
33005
|
+
const url = validateHttpUrlSource(raw.url, `apps[${i}].url`);
|
|
32448
33006
|
const start = validateAppStart(raw.start, `apps[${i}].start`);
|
|
33007
|
+
const stop = validateAppStart(raw.stop, `apps[${i}].stop`);
|
|
32449
33008
|
const legacyAppStart = validateAppStart(
|
|
32450
33009
|
raw.appStart,
|
|
32451
33010
|
`apps[${i}].appStart`
|
|
@@ -32461,18 +33020,24 @@ function validateApps(value) {
|
|
|
32461
33020
|
`tempo.config.json field "apps[${i}].url" requires "apps[${i}].start"`
|
|
32462
33021
|
);
|
|
32463
33022
|
}
|
|
33023
|
+
if (stop !== void 0 && effectiveStart === void 0) {
|
|
33024
|
+
throw new Error(
|
|
33025
|
+
`tempo.config.json field "apps[${i}].stop" requires "apps[${i}].start"`
|
|
33026
|
+
);
|
|
33027
|
+
}
|
|
32464
33028
|
const uiFramework = validateUiFramework(
|
|
32465
33029
|
raw.uiFramework,
|
|
32466
33030
|
`apps[${i}].uiFramework`
|
|
32467
33031
|
);
|
|
32468
|
-
if (appDir === void 0 && effectiveStart === void 0 && url === void 0 && uiFramework === void 0) {
|
|
33032
|
+
if (appDir === void 0 && effectiveStart === void 0 && stop === void 0 && url === void 0 && uiFramework === void 0) {
|
|
32469
33033
|
throw new Error(
|
|
32470
|
-
`tempo.config.json field "apps[${i}]" must set "appDir", "start", "url", and/or "uiFramework" \u2014 an empty entry encodes nothing`
|
|
33034
|
+
`tempo.config.json field "apps[${i}]" must set "appDir", "start", "stop", "url", and/or "uiFramework" \u2014 an empty entry encodes nothing`
|
|
32471
33035
|
);
|
|
32472
33036
|
}
|
|
32473
33037
|
return {
|
|
32474
33038
|
...appDir !== void 0 ? { appDir } : {},
|
|
32475
33039
|
...effectiveStart !== void 0 ? { start: effectiveStart } : {},
|
|
33040
|
+
...stop !== void 0 ? { stop } : {},
|
|
32476
33041
|
...url !== void 0 ? { url } : {},
|
|
32477
33042
|
...uiFramework !== void 0 ? { uiFramework } : {}
|
|
32478
33043
|
};
|
|
@@ -32761,7 +33326,12 @@ async function writeTempoConfigFile(configDir, config) {
|
|
|
32761
33326
|
...normalized.scripts.stop ? { stop: normalized.scripts.stop } : {}
|
|
32762
33327
|
};
|
|
32763
33328
|
if (normalized.apps) {
|
|
32764
|
-
rendered.apps = normalized.apps
|
|
33329
|
+
rendered.apps = normalized.apps.map(
|
|
33330
|
+
({ appStart: _legacyAppStart, ...app }) => {
|
|
33331
|
+
void _legacyAppStart;
|
|
33332
|
+
return app;
|
|
33333
|
+
}
|
|
33334
|
+
);
|
|
32765
33335
|
}
|
|
32766
33336
|
if (normalized.setupScript) {
|
|
32767
33337
|
rendered.setupScript = normalized.setupScript;
|
|
@@ -32847,6 +33417,7 @@ async function resolveTempoConfig(projectRoot) {
|
|
|
32847
33417
|
apps = config.apps.map((app) => ({
|
|
32848
33418
|
appDir: app.appDir,
|
|
32849
33419
|
start: app.start ?? null,
|
|
33420
|
+
stop: app.stop ?? null,
|
|
32850
33421
|
url: app.url ?? null,
|
|
32851
33422
|
appStart: app.start ?? null,
|
|
32852
33423
|
...app.uiFramework ? { uiFramework: app.uiFramework } : {}
|
|
@@ -32857,6 +33428,7 @@ async function resolveTempoConfig(projectRoot) {
|
|
|
32857
33428
|
{
|
|
32858
33429
|
...config.appDir !== void 0 ? { appDir: config.appDir } : {},
|
|
32859
33430
|
start: legacyStart,
|
|
33431
|
+
stop: null,
|
|
32860
33432
|
url: null,
|
|
32861
33433
|
appStart: legacyStart
|
|
32862
33434
|
}
|
|
@@ -32880,7 +33452,8 @@ async function resolveTempoConfig(projectRoot) {
|
|
|
32880
33452
|
install: config.scripts.install,
|
|
32881
33453
|
start: config.scripts.start,
|
|
32882
33454
|
stop: config.scripts.stop ?? null,
|
|
32883
|
-
appStart: activeApp.appStart
|
|
33455
|
+
appStart: activeApp.appStart,
|
|
33456
|
+
appStop: activeApp.stop
|
|
32884
33457
|
},
|
|
32885
33458
|
apps
|
|
32886
33459
|
};
|
|
@@ -41961,7 +42534,7 @@ async function analyzeProject(projectPath) {
|
|
|
41961
42534
|
|
|
41962
42535
|
// ../tempo-project/tempo-project.ts
|
|
41963
42536
|
function configsEqual(left, right) {
|
|
41964
|
-
return left.projectRoot === right.projectRoot && left.tempoConfigPath === right.tempoConfigPath && left.rootRel === right.rootRel && left.tempoRoot === right.tempoRoot && left.pagesDir === right.pagesDir && left.packageJsonPath === right.packageJsonPath && left.viteConfigPath === right.viteConfigPath && left.tsconfigPath === right.tsconfigPath && left.tempoHostUrl === right.tempoHostUrl && left.uiFramework === right.uiFramework && left.scripts.install === right.scripts.install && left.scripts.start === right.scripts.start && left.scripts.stop === right.scripts.stop;
|
|
42537
|
+
return left.projectRoot === right.projectRoot && left.tempoConfigPath === right.tempoConfigPath && left.rootRel === right.rootRel && left.tempoRoot === right.tempoRoot && left.pagesDir === right.pagesDir && left.packageJsonPath === right.packageJsonPath && left.viteConfigPath === right.viteConfigPath && left.tsconfigPath === right.tsconfigPath && JSON.stringify(left.tempoHostUrl) === JSON.stringify(right.tempoHostUrl) && left.uiFramework === right.uiFramework && left.scripts.install === right.scripts.install && left.scripts.start === right.scripts.start && left.scripts.stop === right.scripts.stop;
|
|
41965
42538
|
}
|
|
41966
42539
|
function usesTempoManagedSidecarPackage(config, framework) {
|
|
41967
42540
|
if (!looksLikeGeneratedInstallScript(config.scripts.install)) return false;
|
|
@@ -44474,28 +45047,33 @@ function registerTools(server, context) {
|
|
|
44474
45047
|
);
|
|
44475
45048
|
server.tool(
|
|
44476
45049
|
"set_app_dev_command",
|
|
44477
|
-
"Configure the user's REAL app dev server in tempo.config.json
|
|
45050
|
+
"Configure the user's REAL app dev server in tempo.config.json as `apps[0].start` plus optional `stop` and `url`. `url` is either an HTTP(S) string or `{ command }`; a URL command runs only during launch and must exit 0 with exactly one HTTP(S) URL on stdout. A clean early `start` exit is accepted only with `stop`, after which URL probes own liveness and `stop` owns teardown. Tempo exposes `${APP_PORT}` (legacy `${PORT}` also works) to every command. Legacy appStart spellings still read and migrate. At least one field must be provided; null clears it. Idempotent.",
|
|
44478
45051
|
{
|
|
44479
45052
|
command: z23.string().nullish().describe(
|
|
44480
45053
|
"The canonical apps[0].start foreground command. It may be arbitrary; use `${APP_PORT}` when the command should consume Tempo's allocated helper port. Legacy `${PORT}` remains supported. Omit to leave unchanged; pass null/empty to clear the app server configuration."
|
|
44481
45054
|
),
|
|
44482
|
-
url: z23.string().nullish().describe(
|
|
44483
|
-
|
|
45055
|
+
url: z23.union([z23.string(), z23.object({ command: z23.string().min(1) }).strict()]).nullish().describe(
|
|
45056
|
+
'Optional canonical apps[0].url: either a reachable HTTP(S) URL string or `{ command: "..." }`. The command runs once per launch from appDir (after start exits in the stop-backed handoff shape), receives APP_PORT/PORT/TEMPO_APP_PORT, and must print exactly one HTTP(S) URL. Omit to leave unchanged; pass null/empty to clear.'
|
|
45057
|
+
),
|
|
45058
|
+
stop: z23.string().nullish().describe(
|
|
45059
|
+
"Optional canonical apps[0].stop teardown command. Required when start exits successfully while the app remains reachable externally. Runs on normal stop and stale-runtime reconciliation. Omit to leave unchanged; pass null/empty to clear."
|
|
44484
45060
|
),
|
|
44485
45061
|
...appDevServerTargetArgs
|
|
44486
45062
|
},
|
|
44487
|
-
async ({ command, url, repo_root, app_dir }) => {
|
|
45063
|
+
async ({ command, stop, url, repo_root, app_dir }) => {
|
|
44488
45064
|
if (!context.appDevServer)
|
|
44489
45065
|
return unsupportedEnvironmentResult("appDevServer");
|
|
44490
|
-
if (command === void 0 && url === void 0) {
|
|
44491
|
-
return errorResult9("Provide command and/or url to update.");
|
|
45066
|
+
if (command === void 0 && stop === void 0 && url === void 0) {
|
|
45067
|
+
return errorResult9("Provide command, stop, and/or url to update.");
|
|
44492
45068
|
}
|
|
44493
45069
|
const startValue = command === void 0 ? void 0 : typeof command === "string" && command.trim().length > 0 ? command.trim() : null;
|
|
44494
|
-
const urlValue = url === void 0 ? void 0 : typeof url === "string"
|
|
45070
|
+
const urlValue = url === void 0 ? void 0 : typeof url === "string" ? url.trim().length > 0 ? url.trim() : null : url === null ? null : { command: url.command.trim() };
|
|
45071
|
+
const stopValue = stop === void 0 ? void 0 : typeof stop === "string" && stop.trim().length > 0 ? stop.trim() : null;
|
|
44495
45072
|
try {
|
|
44496
45073
|
await context.appDevServer.setConfig(
|
|
44497
45074
|
{
|
|
44498
45075
|
...startValue !== void 0 ? { start: startValue } : {},
|
|
45076
|
+
...stopValue !== void 0 ? { stop: stopValue } : {},
|
|
44499
45077
|
...urlValue !== void 0 ? { url: urlValue } : {}
|
|
44500
45078
|
},
|
|
44501
45079
|
toAppDevServerTarget({ repo_root, app_dir })
|
|
@@ -44507,11 +45085,13 @@ function registerTools(server, context) {
|
|
|
44507
45085
|
// tool output instead of only the tool arguments.
|
|
44508
45086
|
...startValue !== void 0 ? { app_dev_command: startValue } : {},
|
|
44509
45087
|
...urlValue !== void 0 ? { app_dev_url: urlValue } : {},
|
|
45088
|
+
...stopValue !== void 0 ? { app_dev_stop: stopValue } : {},
|
|
44510
45089
|
app_dev_config_update: {
|
|
44511
45090
|
...startValue !== void 0 ? { start: startValue } : {},
|
|
45091
|
+
...stopValue !== void 0 ? { stop: stopValue } : {},
|
|
44512
45092
|
...urlValue !== void 0 ? { url: urlValue } : {}
|
|
44513
45093
|
},
|
|
44514
|
-
note: startValue === null ? "Cleared." : "Saved. The supervisor allocates
|
|
45094
|
+
note: startValue === null ? "Cleared." : "Saved. The supervisor allocates `${APP_PORT}` independently; static or command-resolved URLs are the readiness target and route base. If start hands off to an external server, stop is required and URL probes become liveness authority. Before declaring setup complete, verify the app depends on tempo-sdk and its own bundler config carries Tempo instrumentation. For standalone Svelte/Vite, add @sveltejs/vite-plugin-svelte only if missing, preserve compatible configured versions, and use tempoVitePlugin() plus tempoSveltePreprocess(...) first in the app's preprocess chain; tempo/ only configures the sidecar."
|
|
44515
45095
|
},
|
|
44516
45096
|
null,
|
|
44517
45097
|
2
|
|
@@ -44526,7 +45106,7 @@ function registerTools(server, context) {
|
|
|
44526
45106
|
);
|
|
44527
45107
|
server.tool(
|
|
44528
45108
|
"check_app_dev_server",
|
|
44529
|
-
"Check the per-workspace app dev server configured by canonical `apps[0].start`/`url` (legacy appStart spellings also read). Returns `{configured, running, phase, port, url, error}
|
|
45109
|
+
"Check the per-workspace app dev server configured by canonical `apps[0].start`/`stop`/`url` (legacy appStart spellings also read). Returns `{configured, running, phase, port, url, livenessSource, error}`; liveness is `child` for foreground servers and `probe` after external handoff. Use this when a route storyboard isn't rendering. `configured: false` means start is unset; `phase: 'error'` carries the supervisor failure.",
|
|
44530
45110
|
{ ...appDevServerTargetArgs },
|
|
44531
45111
|
async ({ repo_root, app_dir }) => {
|
|
44532
45112
|
if (!context.appDevServer)
|
|
@@ -44545,7 +45125,7 @@ function registerTools(server, context) {
|
|
|
44545
45125
|
);
|
|
44546
45126
|
server.tool(
|
|
44547
45127
|
"start_app_dev_server",
|
|
44548
|
-
"Start the per-workspace app dev server if it isn't already running. Spawns canonical `apps[0].start`, substitutes `${APP_PORT}` (or legacy `${PORT}`) in start/url, persists the independently selected local helper port under `<workspaceRoot>/.tempo/app-devserver.json`, and HTTP-gates readiness against the
|
|
45128
|
+
"Start the per-workspace app dev server if it isn't already running. Spawns canonical `apps[0].start`, substitutes `${APP_PORT}` (or legacy `${PORT}`) in start/stop/url commands, persists the independently selected local helper port under `<workspaceRoot>/.tempo/app-devserver.json`, resolves `url.command` once when configured, and HTTP-gates readiness against the resolved/static URL or detected local URL. A clean early start exit requires `apps[0].stop`; then URL probes own liveness and stop owns teardown. A literal URL is probed as written and does not affect local allocation. No-op if already running. When start is unset, returns `{configured: false}`; call `set_app_dev_command` first.",
|
|
44549
45129
|
{ ...appDevServerTargetArgs },
|
|
44550
45130
|
async ({ repo_root, app_dir }) => {
|
|
44551
45131
|
if (!context.appDevServer)
|
|
@@ -44762,7 +45342,7 @@ function registerTools(server, context) {
|
|
|
44762
45342
|
// unpushed/dirty branch before any capture), captures a whole-canvas
|
|
44763
45343
|
// composite screenshot + each storyboard's HTML, saves them as the link's
|
|
44764
45344
|
// preview snapshot, then mints the share URL.
|
|
44765
|
-
"Create a shareable link to a canvas and get back a screenshot of the whole canvas. **The canvas MUST be fully committed and pushed to a remote (clean git state) before sharing** \u2014 exactly like link_items \u2014 otherwise this returns a 'branch_not_pushed' error and you must commit + push the canvas's branch first so the recipient can access the code.
|
|
45345
|
+
"Create a shareable link to a canvas and get back a screenshot of the whole canvas. **The canvas MUST be fully committed and pushed to a remote (clean git state) before sharing** \u2014 exactly like link_items \u2014 otherwise this returns a 'branch_not_pushed' error and you must commit + push the canvas's branch first so the recipient can access the code. Cache-first: when a published preview already exists for the branch's exact pushed commit, the link returns immediately with no re-capture; otherwise it captures a composite screenshot of every storyboard, publishes it as the branch's preview, then mints the link. Share links are BRANCH-LIVE: an existing link (and its public token) keeps its URL and automatically shows the newest published preview each time the branch's pushed canvas is re-captured \u2014 no need to re-share after pushing new commits. Returns the URL plus the whole-canvas screenshot (auto-displayed inline). By default the link is member-only (the recipient opens it as an org member); pass public: true to mint an anyone-with-the-link token instead. Args: canvas_slug (folder name, or root-relative path when the name isn't unique), public (optional, default false).",
|
|
44766
45346
|
{
|
|
44767
45347
|
canvas_slug: z23.string().describe(
|
|
44768
45348
|
"Slug of the canvas \u2014 its folder name, or its path under the canvases root (`design-system/web-design-system/actions`). Pass the PATH whenever the folder name may not be unique; a bare name that matches several canvases is rejected with ambiguous_canvas_slug rather than resolved by guess"
|
|
@@ -45222,7 +45802,7 @@ function isExternallyExposed(toolName, opts) {
|
|
|
45222
45802
|
}
|
|
45223
45803
|
|
|
45224
45804
|
// src/version.ts
|
|
45225
|
-
var CLI_VERSION = "0.0.
|
|
45805
|
+
var CLI_VERSION = "0.0.104";
|
|
45226
45806
|
|
|
45227
45807
|
// src/canvas-hooks.ts
|
|
45228
45808
|
var import_pngjs3 = __toESM(require_png(), 1);
|
|
@@ -47196,4 +47776,4 @@ export {
|
|
|
47196
47776
|
runServe,
|
|
47197
47777
|
scopeParamsFor
|
|
47198
47778
|
};
|
|
47199
|
-
//# sourceMappingURL=serve-
|
|
47779
|
+
//# sourceMappingURL=serve-FWQ5ZBR7.js.map
|