replicas-engine 0.1.440 → 0.1.442

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.
Files changed (2) hide show
  1. package/dist/src/index.js +226 -26
  2. package/package.json +4 -1
package/dist/src/index.js CHANGED
@@ -27,6 +27,21 @@ async function raceWithTimeout(promise, ms) {
27
27
  }
28
28
  }
29
29
 
30
+ // ../shared/src/result.ts
31
+ function createSuccessResult(data) {
32
+ return { ok: true, data };
33
+ }
34
+ function createErrorResult(error) {
35
+ return {
36
+ ok: false,
37
+ error: {
38
+ message: error.message,
39
+ code: error.code,
40
+ details: error.details
41
+ }
42
+ };
43
+ }
44
+
30
45
  // ../shared/src/agent.ts
31
46
  var VALID_AGENT_PROVIDERS = ["claude", "codex", "cursor", "opencode", "relay"];
32
47
  var VALID_CODING_AGENT_PROVIDERS = VALID_AGENT_PROVIDERS.filter(
@@ -506,7 +521,7 @@ var WORKSPACE_SIZES = ["small", "large"];
506
521
  var INVALID_WORKSPACE_SIZE_ERROR = `Invalid size: must be one of ${WORKSPACE_SIZES.join(", ")}`;
507
522
 
508
523
  // ../shared/src/e2b.ts
509
- var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-07-15-v5";
524
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-07-16-v2";
510
525
 
511
526
  // ../shared/src/runtime-env.ts
512
527
  function shellQuotePosix(value) {
@@ -1968,24 +1983,18 @@ When you run services on ports \u2014 such as a web app, API server, or database
1968
1983
 
1969
1984
  ## Running Services for Preview
1970
1985
 
1971
- Services must run as detached background processes so they survive after your command session ends. Do not leave them attached to a foreground terminal.
1986
+ Always start services with \`replicas service start\` (see \`REPLICAS.md\`, "Long-running services"). It daemonizes the process so it survives your turn ending and workspace sleep/wake \u2014 a service left attached to your shell or backgrounded with \`&\`/\`nohup\` dies when the workspace sleeps, leaving the preview broken for the user.
1972
1987
 
1973
- Some potential methods:
1974
1988
  \`\`\`bash
1975
- # Start a detached service with logging
1976
- setsid -f bash -lc 'cd /path/to/app && exec yarn dev >> /tmp/app.log 2>&1'
1977
-
1978
- # For daemons like Docker
1979
- nohup dockerd > /tmp/dockerd.log 2>&1 &
1989
+ replicas service start web "yarn dev" --cwd /path/to/app
1980
1990
  \`\`\`
1981
1991
 
1982
1992
  After starting a service:
1983
- 1. Verify the process is running: \`pgrep -af 'yarn dev'\`
1984
- 2. Check logs for readiness: \`tail -f /tmp/app.log\`
1985
- 3. Confirm it's actually serving: \`curl -s http://localhost:3000\` (or appropriate health check)
1986
- 4. Only create the preview after the service is healthy
1993
+ 1. Check logs for readiness: \`replicas service logs web\`
1994
+ 2. Confirm it's actually serving: \`curl -s http://localhost:3000\` (or appropriate health check)
1995
+ 3. Only create the preview after the service is healthy
1987
1996
 
1988
- If a prior detached process exists on the same port, stop it before restarting.
1997
+ \`replicas service start\` on the same name restarts the service, so you never need to hunt down stale processes on the port.
1989
1998
 
1990
1999
  ## Creating Previews
1991
2000
 
@@ -2058,7 +2067,8 @@ Use this when:
2058
2067
  - The user asks you to create, edit, run, or delete an automation
2059
2068
  - The user asks you to manage environments, environment variables, or environment files
2060
2069
  - The user asks "what envs / repos / automations do I have?"
2061
- - The user asks you to scaffold a \`replicas.json\` / \`replicas.yaml\` in a repo`;
2070
+ - The user asks you to scaffold a \`replicas.json\` / \`replicas.yaml\` in a repo
2071
+ - You need to run a long-lived service (dev server, daemon) \u2014 always use \`replicas service start\` so it survives workspace sleep/wake`;
2062
2072
  var REFERENCE10 = `# Replicas (in-workspace CLI)
2063
2073
 
2064
2074
  This guide covers how to take action *with* Replicas itself from inside a Replicas workspace \u2014 managing automations, environments (and their variables/files), repos, previews, and the user's \`replicas.json\` config \u2014 using the pre-installed \`replicas\` CLI.
@@ -2094,6 +2104,7 @@ In agent mode the CLI hides commands that don't make sense for in-workspace agen
2094
2104
  | \`replicas environment ...\` | Manage environments, env vars, env files |
2095
2105
  | \`replicas automation ...\` | Manage automations (cron + GitHub/GitLab event triggers) |
2096
2106
  | \`replicas preview ...\` | Register / list preview URLs (covered in \`PREVIEWS.md\`) |
2107
+ | \`replicas service ...\` | Run long-lived services detached so they survive workspace sleep/wake (see below) |
2097
2108
  | \`replicas media ...\` | Upload screenshots, videos, audio (covered in \`MEDIA.md\`) |
2098
2109
  | \`replicas slack thread ...\` | Attach or switch Slack thread routing (covered in \`SLACK.md\`) |
2099
2110
 
@@ -2235,6 +2246,24 @@ replicas automation edit <id> \\
2235
2246
 
2236
2247
  \`replicas automation edit <id>\` with no flags drops into interactive mode.
2237
2248
 
2249
+ ## Long-running services
2250
+
2251
+ **Always use \`replicas service start\` to run anything that should keep running after your current command or turn ends** \u2014 dev servers, backend APIs, databases, daemons, watchers. Never leave them attached to your shell, and never rely on plain \`&\`, \`nohup\`, or your own backgrounding: processes started inside your session are torn down when your turn ends and the workspace goes to sleep, so the user finds them dead after waking the workspace. \`replicas service start\` daemonizes the process into its own session, which survives workspace sleep/wake.
2252
+
2253
+ \`\`\`bash
2254
+ replicas service start <name> "<command>" # start (or restart) a named service
2255
+ replicas service start web "bun run dev" --cwd ~/workspaces/app
2256
+ replicas service list # names, pids, running/stopped
2257
+ replicas service logs <name> [-n 100] # tail the service log
2258
+ replicas service stop <name> # stop the whole process group
2259
+ \`\`\`
2260
+
2261
+ Notes:
2262
+ - Quote the command if it contains shell operators: \`replicas service start web "cd app && bun dev"\`.
2263
+ - \`start\` on an existing name restarts it (the old process group is stopped first). It fails fast and prints the log tail if the service dies within the first second.
2264
+ - Logs stream to \`~/.replicas/services/<name>.log\`.
2265
+ - After starting, verify the service is actually healthy (check \`logs\`, then \`curl\` its port) before telling the user it's up or creating a preview for it.
2266
+
2238
2267
  ## Repositories
2239
2268
 
2240
2269
  Read-only listing of repos connected to the org. Use when the user asks "what repos can I use?", or to validate a \`--repository\` value before passing it to \`environment create\` / \`automation create\`:
@@ -3317,7 +3346,7 @@ var MEDIA_KINDS = [MEDIA_KIND.IMAGE, MEDIA_KIND.VIDEO, MEDIA_KIND.AUDIO];
3317
3346
  var SKILL_REGISTRY_MANIFEST_VERSION = 1;
3318
3347
 
3319
3348
  // src/index.ts
3320
- import { randomUUID as randomUUID6 } from "crypto";
3349
+ import { randomUUID as randomUUID7 } from "crypto";
3321
3350
  import { connect } from "net";
3322
3351
 
3323
3352
  // src/utils/exec.ts
@@ -8563,7 +8592,7 @@ var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
8563
8592
  var MIN_CODEX_CLI_VERSION = "0.144.0";
8564
8593
  var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
8565
8594
  var codexCliVersionEnsured = null;
8566
- var ENGINE_PACKAGE_VERSION = "0.1.440";
8595
+ var ENGINE_PACKAGE_VERSION = "0.1.442";
8567
8596
  var INITIALIZE_METHOD = "initialize";
8568
8597
  var INITIALIZED_NOTIFICATION = "initialized";
8569
8598
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -14127,6 +14156,98 @@ ${combinedScript}` : combinedScript;
14127
14156
  return result;
14128
14157
  }
14129
14158
 
14159
+ // src/services/terminal-service.ts
14160
+ import { randomUUID as randomUUID6 } from "crypto";
14161
+ import { existsSync as existsSync9 } from "fs";
14162
+ import { spawn as spawn5 } from "node-pty";
14163
+ var MAX_REPLAY_CHARS = 1024 * 1024;
14164
+ var MAX_TERMINAL_SESSIONS = 8;
14165
+ var TerminalService = class {
14166
+ sessions = /* @__PURE__ */ new Map();
14167
+ nextTitleNumber = 1;
14168
+ list() {
14169
+ return [...this.sessions.values()].map(({ pty: _pty, replay: _replay, subscribers: _subscribers, ...session }) => session);
14170
+ }
14171
+ create(cwd, cols, rows) {
14172
+ if (this.sessions.size >= MAX_TERMINAL_SESSIONS) {
14173
+ return createErrorResult({
14174
+ message: `Terminal session limit reached (${MAX_TERMINAL_SESSIONS})`,
14175
+ code: "limit"
14176
+ });
14177
+ }
14178
+ const id = randomUUID6();
14179
+ const shell = process.env.SHELL && existsSync9(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
14180
+ const pty = spawn5(shell, ["-l"], {
14181
+ name: "xterm-256color",
14182
+ cols,
14183
+ rows,
14184
+ cwd,
14185
+ env: {
14186
+ ...process.env,
14187
+ TERM: "xterm-256color",
14188
+ COLORTERM: "truecolor"
14189
+ }
14190
+ });
14191
+ const session = {
14192
+ id,
14193
+ title: `Terminal ${this.nextTitleNumber++}`,
14194
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
14195
+ exited: false,
14196
+ pty,
14197
+ replay: "",
14198
+ subscribers: /* @__PURE__ */ new Set()
14199
+ };
14200
+ this.sessions.set(id, session);
14201
+ pty.onData((data) => {
14202
+ session.replay = (session.replay + data).slice(-MAX_REPLAY_CHARS);
14203
+ for (const subscriber of session.subscribers) subscriber(data);
14204
+ });
14205
+ pty.onExit(({ exitCode }) => {
14206
+ session.exited = true;
14207
+ session.exitCode = exitCode;
14208
+ const data = `\r
14209
+ \x1B[2m[Process exited with code ${exitCode}]\x1B[0m\r
14210
+ `;
14211
+ session.replay = (session.replay + data).slice(-MAX_REPLAY_CHARS);
14212
+ for (const subscriber of session.subscribers) subscriber(data);
14213
+ });
14214
+ return createSuccessResult(this.publicSession(session));
14215
+ }
14216
+ write(id, data) {
14217
+ const session = this.sessions.get(id);
14218
+ if (!session || session.exited) return false;
14219
+ session.pty.write(data);
14220
+ return true;
14221
+ }
14222
+ resize(id, cols, rows) {
14223
+ const session = this.sessions.get(id);
14224
+ if (!session || session.exited) return false;
14225
+ session.pty.resize(cols, rows);
14226
+ return true;
14227
+ }
14228
+ subscribe(id, subscriber) {
14229
+ const session = this.sessions.get(id);
14230
+ if (!session) return null;
14231
+ session.subscribers.add(subscriber);
14232
+ return {
14233
+ replay: session.replay,
14234
+ unsubscribe: () => session.subscribers.delete(subscriber)
14235
+ };
14236
+ }
14237
+ delete(id) {
14238
+ const session = this.sessions.get(id);
14239
+ if (!session) return false;
14240
+ if (!session.exited) session.pty.kill();
14241
+ this.sessions.delete(id);
14242
+ return true;
14243
+ }
14244
+ publicSession(session) {
14245
+ const { pty: _pty, replay: _replay, subscribers: _subscribers, ...value } = session;
14246
+ return value;
14247
+ }
14248
+ };
14249
+ var terminalService = new TerminalService();
14250
+
14130
14251
  // src/v1-routes.ts
14131
14252
  var setWorkspaceNameSchema = z2.object({
14132
14253
  name: z2.string().min(1).max(48)
@@ -14142,6 +14263,13 @@ var createPreviewSchema = z2.object({
14142
14263
  port: z2.number().int().min(1).max(65535),
14143
14264
  publicUrl: z2.string().min(1)
14144
14265
  });
14266
+ var terminalSizeSchema = z2.object({
14267
+ cols: z2.number().int().min(2).max(500),
14268
+ rows: z2.number().int().min(1).max(200)
14269
+ });
14270
+ var writeTerminalSessionSchema = z2.object({
14271
+ data: z2.string().max(64 * 1024)
14272
+ });
14145
14273
  var sendMessageSchema = z2.object({
14146
14274
  message: z2.string().min(1),
14147
14275
  model: z2.string().optional(),
@@ -14515,6 +14643,78 @@ function createV1Routes(deps) {
14515
14643
  }
14516
14644
  return c.json(result);
14517
14645
  });
14646
+ app2.get("/terminal/sessions", (c) => {
14647
+ return c.json({ sessions: terminalService.list() });
14648
+ });
14649
+ app2.post("/terminal/sessions", async (c) => {
14650
+ const size = terminalSizeSchema.parse(await c.req.json());
14651
+ const result = terminalService.create(gitService.getWorkspaceRoot(), size.cols, size.rows);
14652
+ if (!result.ok) return c.json(jsonError(result.error.message), 429);
14653
+ return c.json({
14654
+ session: result.data
14655
+ }, 201);
14656
+ });
14657
+ app2.post("/terminal/sessions/:id/input", async (c) => {
14658
+ const body = writeTerminalSessionSchema.parse(await c.req.json());
14659
+ if (!terminalService.write(c.req.param("id"), body.data)) {
14660
+ return c.json(jsonError("Terminal session not found or exited"), 404);
14661
+ }
14662
+ return c.body(null, 204);
14663
+ });
14664
+ app2.post("/terminal/sessions/:id/resize", async (c) => {
14665
+ const size = terminalSizeSchema.parse(await c.req.json());
14666
+ if (!terminalService.resize(c.req.param("id"), size.cols, size.rows)) {
14667
+ return c.json(jsonError("Terminal session not found or exited"), 404);
14668
+ }
14669
+ return c.body(null, 204);
14670
+ });
14671
+ app2.delete("/terminal/sessions/:id", (c) => {
14672
+ if (!terminalService.delete(c.req.param("id"))) {
14673
+ return c.json(jsonError("Terminal session not found"), 404);
14674
+ }
14675
+ return c.body(null, 204);
14676
+ });
14677
+ app2.get("/terminal/sessions/:id/stream", (c) => {
14678
+ const encoder = new TextEncoder();
14679
+ let cleanup = () => {
14680
+ };
14681
+ const stream = new ReadableStream({
14682
+ start(controller) {
14683
+ const subscription = terminalService.subscribe(c.req.param("id"), (data) => {
14684
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
14685
+
14686
+ `));
14687
+ });
14688
+ if (!subscription) {
14689
+ controller.enqueue(encoder.encode(`event: error
14690
+ data: ${JSON.stringify("Terminal session not found")}
14691
+
14692
+ `));
14693
+ controller.close();
14694
+ return;
14695
+ }
14696
+ if (subscription.replay) controller.enqueue(encoder.encode(`data: ${JSON.stringify(subscription.replay)}
14697
+
14698
+ `));
14699
+ const heartbeat = setInterval(() => controller.enqueue(encoder.encode(": keepalive\n\n")), 15e3);
14700
+ cleanup = () => {
14701
+ clearInterval(heartbeat);
14702
+ subscription.unsubscribe();
14703
+ };
14704
+ },
14705
+ cancel() {
14706
+ cleanup();
14707
+ }
14708
+ });
14709
+ c.req.raw.signal.addEventListener("abort", cleanup, { once: true });
14710
+ return new Response(stream, {
14711
+ headers: {
14712
+ "Content-Type": "text/event-stream",
14713
+ "Cache-Control": "no-cache",
14714
+ Connection: "keep-alive"
14715
+ }
14716
+ });
14717
+ });
14518
14718
  app2.get("/canvas", async (c) => {
14519
14719
  try {
14520
14720
  const items = await canvasService.listItems();
@@ -15125,7 +15325,7 @@ function startStatusBroadcaster() {
15125
15325
  if (serialized !== previousRepoStatus) {
15126
15326
  previousRepoStatus = serialized;
15127
15327
  eventService.publish({
15128
- id: randomUUID6(),
15328
+ id: randomUUID7(),
15129
15329
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15130
15330
  type: "repo.status.changed",
15131
15331
  payload: { repos }
@@ -15146,7 +15346,7 @@ function startStatusBroadcaster() {
15146
15346
  if (engineStatusJson !== previousEngineStatus) {
15147
15347
  previousEngineStatus = engineStatusJson;
15148
15348
  eventService.publish({
15149
- id: randomUUID6(),
15349
+ id: randomUUID7(),
15150
15350
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15151
15351
  type: "engine.status.changed",
15152
15352
  payload: { status: engineStatus }
@@ -15165,7 +15365,7 @@ function startStatusBroadcaster() {
15165
15365
  previousHookStatus = hookSnapshot;
15166
15366
  if (!lastHooksRunning && hooksRunning) {
15167
15367
  eventService.publish({
15168
- id: randomUUID6(),
15368
+ id: randomUUID7(),
15169
15369
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15170
15370
  type: "hooks.started",
15171
15371
  payload: { running: true, completed: false }
@@ -15174,7 +15374,7 @@ function startStatusBroadcaster() {
15174
15374
  }
15175
15375
  if (hooksRunning) {
15176
15376
  eventService.publish({
15177
- id: randomUUID6(),
15377
+ id: randomUUID7(),
15178
15378
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15179
15379
  type: "hooks.progress",
15180
15380
  payload: { running: true, completed: false }
@@ -15183,7 +15383,7 @@ function startStatusBroadcaster() {
15183
15383
  }
15184
15384
  if (lastHooksRunning && !hooksRunning && hooksCompleted && !hooksFailed) {
15185
15385
  eventService.publish({
15186
- id: randomUUID6(),
15386
+ id: randomUUID7(),
15187
15387
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15188
15388
  type: "hooks.completed",
15189
15389
  payload: { running: false, completed: true }
@@ -15192,7 +15392,7 @@ function startStatusBroadcaster() {
15192
15392
  }
15193
15393
  if (lastHooksRunning && !hooksRunning && hooksFailed) {
15194
15394
  eventService.publish({
15195
- id: randomUUID6(),
15395
+ id: randomUUID7(),
15196
15396
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15197
15397
  type: "hooks.failed",
15198
15398
  payload: { running: false, completed: hooksCompleted }
@@ -15200,7 +15400,7 @@ function startStatusBroadcaster() {
15200
15400
  });
15201
15401
  }
15202
15402
  eventService.publish({
15203
- id: randomUUID6(),
15403
+ id: randomUUID7(),
15204
15404
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15205
15405
  type: "hooks.status",
15206
15406
  payload: {
@@ -15252,20 +15452,20 @@ serve(
15252
15452
  }
15253
15453
  const repos = await gitService.listRepos();
15254
15454
  await eventService.publish({
15255
- id: randomUUID6(),
15455
+ id: randomUUID7(),
15256
15456
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15257
15457
  type: "repo.discovered",
15258
15458
  payload: { repos }
15259
15459
  });
15260
15460
  const repoStatuses = await gitService.listRepos();
15261
15461
  await eventService.publish({
15262
- id: randomUUID6(),
15462
+ id: randomUUID7(),
15263
15463
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15264
15464
  type: "repo.status.changed",
15265
15465
  payload: { repos: repoStatuses }
15266
15466
  });
15267
15467
  await eventService.publish({
15268
- id: randomUUID6(),
15468
+ id: randomUUID7(),
15269
15469
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15270
15470
  type: "engine.ready",
15271
15471
  payload: { version: "v1" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.440",
3
+ "version": "0.1.442",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -43,6 +43,9 @@
43
43
  "yaml": "^2.8.2",
44
44
  "zod": "^4.0.0"
45
45
  },
46
+ "optionalDependencies": {
47
+ "node-pty": "1.2.0-beta.14"
48
+ },
46
49
  "devDependencies": {
47
50
  "@replicas/codex-asp-types": "file:../codex-asp-types",
48
51
  "@replicas/shared": "workspace:*",