portomatic 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # portomatic
2
2
 
3
+ Coding agents: read `portomatic slack docs agents` first.
4
+
3
5
  Local emulators for the services your agents use. Node ≥24; no accounts or credentials needed.
4
6
 
5
7
  ```sh
6
8
  npx portomatic slack start --share --headless --port 0 --json
7
9
  ```
8
10
 
9
- The ready result gives `url` (the local service) and `dashboard` (your private web view at `<device>.portomatic.com`). Then drive it with the same CLI:
11
+ The ready result gives `url` (the local service) and `dashboard` (your private web view at `https://<name>.portomatic.com`). Then drive it with the same CLI:
10
12
 
11
13
  ```sh
12
14
  portomatic slack seed --seed workspace.json --endpoint <url> --json
@@ -14,8 +16,12 @@ portomatic slack status --endpoint <url> --json
14
16
  portomatic slack stop --endpoint <url>
15
17
  ```
16
18
 
19
+ This computer has one dashboard, `https://<name>.portomatic.com`; every emulator started with `--share` appears there automatically at `/slack/<id>`. For another emulator, run `portomatic slack start --share --port 0` again: each gets its own `url` and workspace; seed each with `--endpoint <its url>`. The dashboard link stays the same; `portomatic slack dashboard` prints it. Read `portomatic slack docs seed` for the seed format.
20
+
17
21
  `portomatic` lists products. `portomatic slack help --all` lists commands; coding agents start with `portomatic slack docs agents`.
18
22
 
19
23
  `--share` (or `PORTOMATIC_SHARE=1`) publishes runs to this machine's private dashboard. The emulator connects out over one WebSocket to `relay.portomatic.com` and never listens for inbound connections; UI traffic passes through the relay without being stored, and only run metadata is kept. Sharing is off unless you ask for it.
20
24
 
21
- Portomatic collects anonymous product usage (no message text, names, tokens, seeds or payloads). Set `PORTOMATIC_TELEMETRY_DISABLED=1` or `DO_NOT_TRACK=1` to turn it off; `PORTOMATIC_TELEMETRY_DEBUG=1` prints what would be sent.
25
+ ## Telemetry
26
+
27
+ Portomatic collects anonymous product usage (no message text, names, tokens, seeds or payloads). Set `PORTOMATIC_TELEMETRY_DISABLED=1` or `DO_NOT_TRACK=1` to turn it off.
@@ -130,9 +130,9 @@ if (runtime?.fresh) {
130
130
  console.log(version);
131
131
  } else if (!products[product]) {
132
132
  const list = Object.entries(products).map(([name, p]) => ` ${name.padEnd(8)} ${p.summary}`).join('\n');
133
- const usage = `Portomatic: local emulators for the services your agents use.\n\nUSAGE portomatic <product> [command] [flags]\n\nPRODUCTS\n${list}\n\nEXAMPLE portomatic slack start --share --headless --port 0 --json\nMORE portomatic slack help | portomatic slack docs agents\n`;
133
+ const usage = `Portomatic: local emulators for the services your agents use.\nCoding agents: read portomatic slack docs agents first.\n\nUSAGE portomatic <product> [command] [flags]\n\nPRODUCTS\n${list}\n\nEXAMPLE portomatic slack start --share --headless --port 0 --json\nDASHBOARD Every emulator started with --share on this computer appears on the same dashboard at /slack/<id>.\nSEVERAL Run portomatic slack start --share --port 0 again for its own url and workspace.\nSEED portomatic slack docs seed (seed format)\nMORE portomatic slack help | portomatic slack docs agents\n`;
134
134
  const asked = !product || ['help', '--help', '-h'].includes(product);
135
- (asked ? process.stdout : process.stderr).write(asked ? usage : `Unknown product '${product}'.\n\n${usage}`);
135
+ (asked ? process.stdout : process.stderr).write(asked ? usage : `Error, please contact founders@portomatic.com\n\n${usage}`);
136
136
  process.exitCode = asked ? 0 : 1;
137
137
  } else {
138
138
  const main = new URL(products[product].main, import.meta.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portomatic",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Local emulators for the services your agents use. Start with: npx portomatic slack start --share",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- var e={agents:"Local Slack you drive from the CLI: start a workspace, seed it, act in it, inspect it.\n\n1. Start: `npx portomatic slack start --share --headless --port 0 --json`. Keep it running.\n The ready result has `url` (local Slack) and `dashboard` (the user's private web view).\n2. Give the user the `dashboard` link so they can watch and take part in the browser.\n3. Seed: `portomatic slack seed --seed workspace.json --endpoint <url> --json`. Use the returned IDs and credentials, not seed names.\n4. Act and inspect through the control plane at `<url>/_emu/` (post, react, click, slash, upload; read messages and deliveries). `portomatic slack docs control-plane` lists every route.\n5. Check health with `status`, `doctor` and `logs --follow`; reset with `reset`; save and restore with `snapshot save|load NAME`.\n6. Stop with `portomatic slack stop --endpoint <url>`.\n\nEvery command except `start` targets a running workspace: pass `--endpoint <url>` from the ready result (default `http://127.0.0.1:3000`).\nUse `run -- <cmd>` to start a workspace and run a bot against it with credentials injected; it accepts `--share` too.\n`portomatic slack dashboard` reprints the dashboard link. `PORTOMATIC_SHARE=1` shares without the flag.\nUse `--json` for one result envelope (`run` and `mcp` reject it). `help --all` lists commands; `docs` lists offline topics.\nDo not read package internals or guess callback payloads; use control-plane actors and observations.\n",quickstart:"```sh\nnpm i -D portomatic\nnpx portomatic slack init\nnode --test slack.integration.test.mjs\nnpx portomatic slack run -- node app.js\n```\n\nKeep `manifest.json` (or YAML) in the project root. Use `--manifest FILE` elsewhere.\n\n```sh\nportomatic slack --port 0 --headless --json\nportomatic slack --endpoint http://127.0.0.1:3000 status --json\nportomatic slack reset\nportomatic slack snapshot save before\nportomatic slack snapshot load before\nportomatic slack snapshot list\nportomatic slack stop\n```\n\nFlags override `EMU_PORT`, `EMU_HOST`, `EMU_SEED`, `EMU_SNAPSHOT_PATH`.\nPaths resolve from cwd. `--snapshots :memory:` discards snapshots on exit.\nThe CLI never loads `.env`. Defaults: port 3000, host 127.0.0.1, snapshots `.emu/snapshots.sqlite`.\nFor bot setup, read `docs bolt-js` or `docs slack-sdk`.\n",api:`\`\`\`ts
1
+ var e={agents:"This computer has one dashboard, `https://<name>.portomatic.com`. Every emulator started with `--share` on this computer appears there automatically at `/slack/<id>`, with its own `url` and workspace. To add another emulator, run `portomatic slack start --share --port 0` again. The dashboard link stays the same; `portomatic slack dashboard` prints it.\n\n1. Start: `portomatic slack start --share --headless --port 0 --json`. Keep it running; the ready result has `url` (local Slack) and `dashboard` (the private web view). Use `start --seed FILE` to seed at startup.\n2. Give the user the `dashboard` link.\n3. Seed: read `portomatic slack docs seed`, then run `portomatic slack seed --seed workspace.json --endpoint <url> --json`. Use the returned IDs and credentials; pass this emulator's `--endpoint <url>` to commands targeting it.\n4. Act through control-plane actors at `<url>/_emu/` to post, react, click, slash and upload. Read `portomatic slack docs control-plane` for routes and actor inputs.\n5. Inspect messages and deliveries through control-plane observations; check health with `status`, `doctor` and `logs --follow`. Reset with `reset`; save and restore with `snapshot save|load NAME`.\n6. More emulators: run `portomatic slack start --share --headless --port 0 --json` again for each one, then seed and target its own `url`; all appear on the same dashboard.\n7. Stop: `portomatic slack stop --endpoint <url>`.\n\nThe default `.emu/snapshots.sqlite` shares snapshot names between emulators in one directory; use a different `--snapshots FILE` for each emulator to keep saved snapshots independent.\n\nRead `portomatic slack help --all` for commands and `portomatic slack docs` for offline topics. Use `--json` for one result envelope; `run`, `mcp` and `logs --follow` use streams. Use `run -- <cmd>` to start a workspace and run a bot with credentials injected; it accepts `--share` too.\n\nAnswer questions from help and docs, not from package source.\n",quickstart:"```sh\nnpm i -D portomatic\nnpx portomatic slack init\nnode --test slack.integration.test.mjs\nnpx portomatic slack run -- node app.js\n```\n\nKeep `manifest.json` (or YAML) in the project root. Use `--manifest FILE` elsewhere.\n\n```sh\nportomatic slack --port 0 --headless --json\nportomatic slack --endpoint http://127.0.0.1:3000 status --json\nportomatic slack reset\nportomatic slack snapshot save before\nportomatic slack snapshot load before\nportomatic slack snapshot list\nportomatic slack stop\n```\n\nFlags override `EMU_PORT`, `EMU_HOST`, `EMU_SEED`, `EMU_SNAPSHOT_PATH`.\nPaths resolve from cwd. `--snapshots :memory:` discards snapshots on exit.\nThe CLI never loads `.env`. Defaults: port 3000, host 127.0.0.1, snapshots `.emu/snapshots.sqlite`.\nFor bot setup, read `docs bolt-js` or `docs slack-sdk`.\n",api:`\`\`\`ts
2
2
  // import values/types from 'portomatic/slack/testing'; Promise results are awaited.
3
3
  type Seed = import("portomatic/slack/testing").Seed /* docs seed; full schema docs/SEED-FORMAT.md */;
4
4
  type Credentials = { user?: string; client?: string; bot?: string; app?: string; signing_secret?: string; webhook?: string; }; type AppCredentials = Credentials & { bot: string; app: string; signing_secret: string; };
@@ -1,10 +1,10 @@
1
- var y=Object.defineProperty;var s=(e,t)=>y(e,"name",{value:t,configurable:!0});function h(e,t=!!process.stdout.isTTY,r=process.env){const n=e["non-interactive"]||e.agent==="yes"||e.agent!=="no"&&(!t||r.CI||r.SSH_CONNECTION);return{format:e["log-format"]??(n||!t?"json":"human"),quiet:!!e.quiet,verbose:!!e.verbose,color:t&&!n&&r.NO_COLOR===void 0}}s(h,"logOptions");function w(e,t){if(t.quiet&&e.level!=="error")return"";const{bodies:r,...n}=e;if(t.format==="json")return JSON.stringify(t.verbose&&!t.quiet?e:n)+`
2
- `;const a=new Date(e.time),l=[a.getHours(),a.getMinutes(),a.getSeconds()].map(o=>String(o).padStart(2,"0")).join(":"),c=e.type.padEnd(12),d=t.color?`\x1B[${e.level==="error"?"31":"2"}m${c}\x1B[0m`:c;let i=`${l} ${d} ${e.message.replace(/[\x00-\x1f\x7f-\x9f]/g," ")}
1
+ var y=Object.defineProperty;var s=(e,t)=>y(e,"name",{value:t,configurable:!0});import{USER_ERROR as p}from"../errors.js";function S(e,t=!!process.stdout.isTTY,r=process.env){const n=e["non-interactive"]||e.agent==="yes"||e.agent!=="no"&&(!t||r.CI||r.SSH_CONNECTION);return{format:e["log-format"]??(n||!t?"json":"human"),quiet:!!e.quiet,verbose:!!e.verbose,color:t&&!n&&r.NO_COLOR===void 0}}s(S,"logOptions");function w(e,t){if(t.quiet&&e.level!=="error")return"";const{bodies:r,...n}=e;if(t.format==="json")return JSON.stringify(t.verbose&&!t.quiet?e:n)+`
2
+ `;const c=new Date(e.time),l=[c.getHours(),c.getMinutes(),c.getSeconds()].map(o=>String(o).padStart(2,"0")).join(":"),a=e.type.padEnd(12),d=t.color?`\x1B[${e.level==="error"?"31":"2"}m${a}\x1B[0m`:a;let i=`${l} ${d} ${e.message.replace(/[\x00-\x1f\x7f-\x9f]/g," ")}
3
3
  `;if(t.verbose&&!t.quiet&&r!==void 0){const o=JSON.stringify(r,null,2);i+=(o.length>4096?o.slice(0,4096)+`
4
4
  \u2026 [truncated]`:o).split(`
5
5
  `).map(f=>" "+f).join(`
6
6
  `)+`
7
- `}return i}s(w,"formatActivity");const p=s((e,t=r=>process.stdout.write(r))=>r=>{const n=w(r,e);n&&t(n)},"activityWriter");async function O(e,t,r=p(t)){const n=new AbortController,a=await fetch(`${e}/_emu/activity?verbose=${t.verbose&&!t.quiet?"1":"0"}&quiet=${t.quiet?"1":"0"}`,{signal:n.signal});if(!a.ok||!a.body)throw new Error(`Activity stream unavailable at ${e}; restart Slack with the current CLI.`);const l=a.body.getReader(),c=(async()=>{const d=new TextDecoder;let i="";try{for(;;){const{done:o,value:f}=await l.read();if(o)break;i+=d.decode(f,{stream:!0});let u;for(;(u=i.indexOf(`
7
+ `}return i}s(w,"formatActivity");const x=s((e,t=r=>process.stdout.write(r))=>r=>{const n=w(r,e);n&&t(n)},"activityWriter");async function q(e,t,r=x(t)){const n=new AbortController,c=await fetch(`${e}/_emu/activity?verbose=${t.verbose&&!t.quiet?"1":"0"}&quiet=${t.quiet?"1":"0"}`,{signal:n.signal});if(!c.ok||!c.body)throw new Error(p);const l=c.body.getReader(),a=(async()=>{const d=new TextDecoder;let i="";try{for(;;){const{done:o,value:f}=await l.read();if(o)break;i+=d.decode(f,{stream:!0});let u;for(;(u=i.indexOf(`
8
8
 
9
- `))>=0;){const b=i.slice(0,u);i=i.slice(u+2);const g=b.split(`
10
- `).find(m=>m.startsWith("data: "));g&&r(JSON.parse(g.slice(6)))}}}catch(o){if(!n.signal.aborted)throw o}finally{l.releaseLock()}})();return c.catch(()=>{}),Object.assign(async()=>{n.abort(),await c},{done:c})}s(O,"followActivity");export{p as activityWriter,O as followActivity,w as formatActivity,h as logOptions};
9
+ `))>=0;){const m=i.slice(0,u);i=i.slice(u+2);const g=m.split(`
10
+ `).find(b=>b.startsWith("data: "));g&&r(JSON.parse(g.slice(6)))}}}catch(o){if(!n.signal.aborted)throw o}finally{l.releaseLock()}})();return a.catch(()=>{}),Object.assign(async()=>{n.abort(),await a},{done:a})}s(q,"followActivity");export{x as activityWriter,q as followActivity,w as formatActivity,S as logOptions};
@@ -1,39 +1,46 @@
1
- var c=Object.defineProperty;var r=(s,n)=>c(s,"name",{value:n,configurable:!0});const o={start:["Start a seeded workspace (default command).","[--seed FILE | --manifest FILE] [--port N] [--headless]","portomatic slack --headless --port 0 --json","docs testing"],run:["Run an existing bot with local credentials and SDK endpoints.","[--seed FILE | --manifest FILE] [--request-url URL] -- COMMAND [ARGS...]",`portomatic slack run -- node app.js
1
+ var c=Object.defineProperty;var r=(t,a)=>c(t,"name",{value:a,configurable:!0});const d="Every emulator started with --share on this computer appears on this same dashboard; start another with --port 0.",o={start:["Start a seeded workspace (default command).","[--seed FILE | --manifest FILE] [--port N] [--headless]","portomatic slack --headless --port 0 --json","docs testing"],run:["Run an existing bot with local credentials and SDK endpoints.","[--seed FILE | --manifest FILE] [--request-url URL] -- COMMAND [ARGS...]",`portomatic slack run -- node app.js
2
2
  portomatic slack run -- npm test`,"doctor"],env:["Print SDK environment variables.","[--seed FILE] [--app NAME]","portomatic slack env --seed workspace.json","docs bolt-js"],seed:["Replace the workspace; return IDs and credentials.","--seed FILE","portomatic slack seed --seed workspace.json --json","doctor"],reset:["Restore the last seed; clear runtime state.","","portomatic slack reset --json","status"],snapshot:["Save, load or list named snapshots.","save NAME | load NAME | list",`portomatic slack snapshot save before
3
3
  portomatic slack snapshot load before`,"status"],status:["Show readiness and pending work.","","portomatic slack --endpoint http://127.0.0.1:4000 status --json","logs"],stop:["Stop the target emulator.","","portomatic slack stop","start"],logs:["Read activity; follow a running workspace.","[--follow]","portomatic slack logs --follow --endpoint http://127.0.0.1:3000","doctor"],doctor:["Check receiver, signature, scopes, events and transport.","[--manifest FILE]","portomatic slack doctor --json","docs troubleshooting"],init:["Detect SDK; create a seed and sample test without overwriting.","[--manifest FILE]","portomatic slack init","docs testing"],docs:["Read bundled documentation offline.","[TOPIC]","portomatic slack docs bolt-js","docs testing"],mcp:["Serve coding-agent tools over stdio.","[--seed FILE]","portomatic slack mcp","docs testing"],dashboard:["Print this machine's private dashboard link (start/run --share publish to it).","",`portomatic slack dashboard
4
- portomatic slack run --share -- npm test`,"start --share"]},i=`--endpoint URL Reuse a server (default http://127.0.0.1:3000)
4
+ portomatic slack run --share -- npm test`,"start --share"]},l=`--endpoint URL Reuse a server (default http://127.0.0.1:3000)
5
5
  --json One result envelope (unsupported: run, mcp, logs --follow)
6
- --non-interactive / --agent auto|yes|no Suppress browser; automatic off-TTY`;function p(s,n=!1){if(n)return Object.entries(o).flatMap(([t,a])=>t==="snapshot"?["snapshot save NAME","snapshot load NAME","snapshot list"]:[t+(a[1]?" "+a[1]:"")]).join(`
6
+ --non-interactive / --agent auto|yes|no Suppress browser; automatic off-TTY`;function h(t,a=!1){if(a)return Object.entries(o).flatMap(([s,n])=>s==="snapshot"?["snapshot save NAME","snapshot load NAME","snapshot list"]:[s+(n[1]?" "+n[1]:"")]).join(`
7
7
  `)+`
8
- `;const e=s&&o[s];return e?`${e[0]}
9
-
8
+ `;const e=t&&o[t],i=t==="start"?`
9
+ Every emulator started with --share on this computer appears on the same https://<name>.portomatic.com dashboard at /slack/<id>.
10
+ Another emulator: run portomatic slack start --share --port 0 again for its own url and workspace; no other flags or environment variables are needed.
11
+ Seed format: portomatic slack docs seed. Coding agents: portomatic slack docs agents.
12
+ `:t==="seed"?`
13
+ Seed format: portomatic slack docs seed.
14
+ `:"";return e?`${e[0]}
15
+ ${i}
10
16
  USAGE
11
- portomatic slack ${s} ${e[1]}
17
+ portomatic slack ${t} ${e[1]}
12
18
 
13
19
  FLAGS
14
- ${i}
15
- ${s==="start"?`--port N / --host ADDRESS / --snapshots FILE
20
+ ${l}
21
+ ${t==="start"?`--port N / --host ADDRESS / --snapshots FILE
16
22
  --headless Disable the default system-browser UI
17
- `:""}${["start","run","logs"].includes(s)?`--verbose / --quiet Include bodies / errors only
23
+ `:""}${["start","run","logs"].includes(t)?`--verbose / --quiet Include bodies / errors only
18
24
  --log-format human|json TTY lines / off-TTY NDJSON; NO_COLOR disables colour
19
- `:""}${["start","run"].includes(s)?`--share Publish to this machine's private *.portomatic.com dashboard (or PORTOMATIC_SHARE=1)
20
- `:""}${s==="run"?`--request-url URL HTTP callback (default http://127.0.0.1:$PORT/slack/events, PORT=3000)
25
+ `:""}${["start","run"].includes(t)?`--share Publish to this machine's private *.portomatic.com dashboard (or PORTOMATIC_SHARE=1)
26
+ `:""}${t==="run"?`--request-url URL HTTP callback (default http://127.0.0.1:$PORT/slack/events, PORT=3000)
21
27
  `:""}
22
28
  EXAMPLES
23
29
  ${e[2].split(`
24
- `).map(t=>" "+t).join(`
30
+ `).map(s=>" "+s).join(`
25
31
  `)}
26
32
 
27
33
  next: portomatic slack ${e[3]}
28
34
  `:`Local Slack for testing Slack integrations.
35
+ Coding agents: read portomatic slack docs agents first.
29
36
 
30
37
  USAGE portomatic slack [command] [flags]
31
38
 
32
39
  WORKSPACE
33
- ${["start","seed","reset","snapshot","status","stop","logs"].map(t=>` ${t.padEnd(9)} ${o[t][0]}`).join(`
40
+ ${["start","seed","reset","snapshot","status","stop","logs"].map(s=>` ${s.padEnd(9)} ${o[s][0]}`).join(`
34
41
  `)}
35
42
  DEVELOP
36
- ${["run","env","init","doctor","docs","mcp"].map(t=>` ${t.padEnd(9)} ${o[t][0]}`).join(`
43
+ ${["run","env","init","doctor","docs","mcp"].map(s=>` ${s.padEnd(9)} ${o[s][0]}`).join(`
37
44
  `)}
38
45
 
39
46
  FLAGS --endpoint URL --json --non-interactive --agent auto|yes|no
@@ -44,8 +51,8 @@ EXAMPLES
44
51
  portomatic slack --port 0 --headless --json
45
52
 
46
53
  AGENT GUIDANCE
47
- Read docs agents first: start --share, seed, act, inspect.
54
+ For several emulators, run start again with --share --port 0; use the same dashboard.
48
55
  Use --json for one envelope; run/mcp reject it.
49
56
  Use help --all for the command tree.
50
- LEARN MORE <command> --help | docs [topic] | docs agents
51
- `}r(p,"help");export{o as commands,p as help};
57
+ LEARN MORE <command> --help | docs [topic] | docs seed (seed format)
58
+ `}r(h,"help");export{o as commands,d as dashboardNote,h as help};
@@ -1,2 +1,2 @@
1
- var g=Object.defineProperty;var c=(m,r)=>g(m,"name",{value:r,configurable:!0});import{createInterface as I}from"node:readline";import{startSlack as S}from"../testing/index.js";const e={type:"string"},p={type:"object",additionalProperties:!0},k={start:{description:"Start isolated Slack; return URL, named IDs and credentials.",properties:{seed:p,manifest:p}},seed:{description:"Replace workspace from a version:1 seed.",properties:{seed:p},required:["seed"]},reset:{description:"Restore seed and clear runtime data.",properties:{}},post:{description:"Post as a seeded human; returns message coordinates.",properties:{user:e,channel:e,text:e,thread:e},required:["user","channel","text"]},react:{description:"React as a human.",properties:{user:e,channel:e,ts:e,name:e,remove:{type:"boolean"}},required:["user","channel","ts","name"]},slash:{description:"Invoke a slash command through the bot transport.",properties:{user:e,channel:e,command:e,text:e},required:["user","channel","command"]},click:{description:"Click the actual message button by actionId.",properties:{user:e,channel:e,ts:e,actionId:e},required:["user","channel","ts","actionId"]},select:{description:"Select an option by value.",properties:{user:e,channel:e,ts:e,actionId:e,value:e},required:["user","channel","ts","actionId","value"]},submit:{description:"Submit a modal with Block Kit state.values.",properties:{user:e,view:e,values:p},required:["user","view"]},open_home:{description:"Open an app Home as a human.",properties:{user:e,app:e},required:["user"]},upload:{description:"Upload UTF-8 file content as a human.",properties:{user:e,channel:e,filename:e,content:e},required:["user","channel","filename","content"]},edit:{description:"Edit a human message.",properties:{user:e,channel:e,ts:e,text:e},required:["user","channel","ts","text"]},delete:{description:"Delete a human message.",properties:{user:e,channel:e,ts:e},required:["user","channel","ts"]},history:{description:"Read channel or thread messages.",properties:{channel:e,thread:e},required:["channel"]},deliveries:{description:"Read full signed callbacks, acknowledgements and errors.",properties:{}},ephemeral:{description:"Read private ephemeral messages for a human.",properties:{user:e},required:["user"]},views:{description:"Read a human modal/Home views.",properties:{user:e},required:["user"]},wait:{description:"Wait for message/reply/delivery/ephemeral/view matching top-level fields.",properties:{query:{type:"object",properties:{message:e,reply:{type:"object",properties:{channel:e,ts:e},required:["channel","ts"]},delivery:{anyOf:[{type:"boolean"},e]},ephemeral:e,view:e,timeout:{type:"number"}},additionalProperties:!1},match:p},required:["query"]},idle:{description:"Wait for admitted effects, acks and retries to drain.",properties:{timeout:{type:"number"}}},set_request_url:{description:"Bind HTTP callback after bot starts on a free port.",properties:{url:e,app:e},required:["url"]},close:{description:"Close Slack and release all resources.",properties:{}}};async function _(m){let r;const h=c(l=>process.stdout.write(JSON.stringify(l)+`
2
- `),"send"),f=I({input:process.stdin,crlfDelay:1/0});let w=Promise.resolve();const b=c(async()=>{f.close(),await r?.close(),r=void 0},"close"),u=c(()=>{b()},"stop");process.once("SIGINT",u),process.once("SIGTERM",u),f.on("line",l=>{w=w.then(async()=>{let i;try{i=JSON.parse(l)}catch{h({jsonrpc:"2.0",id:null,error:{code:-32700,message:"Invalid JSON"}});return}if(i.id===void 0)return;const d=c(o=>h({jsonrpc:"2.0",id:i.id,result:o}),"reply");try{if(i.method==="initialize"){d({protocolVersion:"2024-11-05",capabilities:{tools:{}},serverInfo:{name:"portomatic-slack",version:"0.1.0"}});return}if(i.method==="ping"){d({});return}if(i.method==="tools/list"){d({tools:Object.entries(k).map(([s,n])=>({name:"slack_"+s,description:n.description,inputSchema:{type:"object",properties:n.properties,required:n.required??[],additionalProperties:!1}}))});return}if(i.method!=="tools/call"){h({jsonrpc:"2.0",id:i.id,error:{code:-32601,message:"Method not found"}});return}const o=i.params?.name?.replace(/^slack_/,""),t=i.params?.arguments??{};if(!k[o])throw new Error("Unknown tool; call tools/list");for(const s of k[o].required??[])if(t[s]===void 0)throw new Error(`Missing ${s}`);let a;if(o==="start")await r?.close(),r=await S(t.seed||t.manifest?t:{seed:m}),a={url:r.url,apiUrl:r.apiUrl,ids:r.ids,tokens:r.tokens};else{if(!r)throw new Error("Call slack_start first");const s=t.user?r.as(t.user):void 0,n={channel:r.channel(t.channel??""),ts:t.ts};switch(o){case"seed":await r.control("seed?token_format=structured",t.seed),await r.initialize(),a={ids:r.ids,tokens:r.tokens};break;case"reset":await r.reset(),a={ids:r.ids,tokens:r.tokens};break;case"post":a=await s.post(t.channel,t.text,{thread:t.thread});break;case"react":a=await s.react(n,t.name,{remove:t.remove});break;case"slash":a=await s.slash(t.channel,t.command,t.text);break;case"click":a=await s.clickButton({message:n,actionId:t.actionId});break;case"select":a=await s.selectOption({message:n,actionId:t.actionId,value:t.value});break;case"submit":a=await s.submitView(t.view,t.values);break;case"open_home":a=await s.openHome(t.app);break;case"upload":a=await s.upload(t.channel,t.filename,t.content);break;case"edit":a=await s.edit(n,t.text);break;case"delete":a=await s.delete(n);break;case"history":a=await r.messages(t.channel,t.thread);break;case"deliveries":a=await r.deliveries();break;case"ephemeral":a=await r.ephemeral(t.user);break;case"views":a=await r.views(t.user);break;case"wait":a=await r.waitFor(t.query,v=>Object.entries(t.match??{}).every(([y,q])=>v[y]===q));break;case"idle":await r.idle(t),a={idle:!0};break;case"set_request_url":await r.setRequestUrl(t.url,t.app),a={url:t.url};break;case"close":await r.close(),r=void 0,a={closed:!0};break}}d({content:[{type:"text",text:JSON.stringify(a??{})}]})}catch(o){d({isError:!0,content:[{type:"text",text:o.message}]})}}).catch(i=>console.error(i))}),await new Promise(l=>f.once("close",l)),await w,await r?.close(),process.off("SIGINT",u),process.off("SIGTERM",u)}c(_,"serveMcp");export{_ as serveMcp};
1
+ var I=Object.defineProperty;var l=(h,t)=>I(h,"name",{value:t,configurable:!0});import{createInterface as R}from"node:readline";import{startSlack as S}from"../testing/index.js";import{USER_ERROR as o}from"../errors.js";const e={type:"string"},u={type:"object",additionalProperties:!0},b={start:{description:"Start isolated Slack; return URL, named IDs and credentials.",properties:{seed:u,manifest:u}},seed:{description:"Replace workspace from a version:1 seed.",properties:{seed:u},required:["seed"]},reset:{description:"Restore seed and clear runtime data.",properties:{}},post:{description:"Post as a seeded human; returns message coordinates.",properties:{user:e,channel:e,text:e,thread:e},required:["user","channel","text"]},react:{description:"React as a human.",properties:{user:e,channel:e,ts:e,name:e,remove:{type:"boolean"}},required:["user","channel","ts","name"]},slash:{description:"Invoke a slash command through the bot transport.",properties:{user:e,channel:e,command:e,text:e},required:["user","channel","command"]},click:{description:"Click the actual message button by actionId.",properties:{user:e,channel:e,ts:e,actionId:e},required:["user","channel","ts","actionId"]},select:{description:"Select an option by value.",properties:{user:e,channel:e,ts:e,actionId:e,value:e},required:["user","channel","ts","actionId","value"]},submit:{description:"Submit a modal with Block Kit state.values.",properties:{user:e,view:e,values:u},required:["user","view"]},open_home:{description:"Open an app Home as a human.",properties:{user:e,app:e},required:["user"]},upload:{description:"Upload UTF-8 file content as a human.",properties:{user:e,channel:e,filename:e,content:e},required:["user","channel","filename","content"]},edit:{description:"Edit a human message.",properties:{user:e,channel:e,ts:e,text:e},required:["user","channel","ts","text"]},delete:{description:"Delete a human message.",properties:{user:e,channel:e,ts:e},required:["user","channel","ts"]},history:{description:"Read channel or thread messages.",properties:{channel:e,thread:e},required:["channel"]},deliveries:{description:"Read full signed callbacks, acknowledgements and errors.",properties:{}},ephemeral:{description:"Read private ephemeral messages for a human.",properties:{user:e},required:["user"]},views:{description:"Read a human modal/Home views.",properties:{user:e},required:["user"]},wait:{description:"Wait for message/reply/delivery/ephemeral/view matching top-level fields.",properties:{query:{type:"object",properties:{message:e,reply:{type:"object",properties:{channel:e,ts:e},required:["channel","ts"]},delivery:{anyOf:[{type:"boolean"},e]},ephemeral:e,view:e,timeout:{type:"number"}},additionalProperties:!1},match:u},required:["query"]},idle:{description:"Wait for admitted effects, acks and retries to drain.",properties:{timeout:{type:"number"}}},set_request_url:{description:"Bind HTTP callback after bot starts on a free port.",properties:{url:e,app:e},required:["url"]},close:{description:"Close Slack and release all resources.",properties:{}}};async function O(h){let t;const f=l(d=>process.stdout.write(JSON.stringify(d)+`
2
+ `),"send"),w=R({input:process.stdin,crlfDelay:1/0});let k=Promise.resolve();const v=l(async()=>{w.close(),await t?.close(),t=void 0},"close"),m=l(()=>{v()},"stop");process.once("SIGINT",m),process.once("SIGTERM",m),w.on("line",d=>{k=k.then(async()=>{let i;try{i=JSON.parse(d)}catch{f({jsonrpc:"2.0",id:null,error:{code:-32700,message:o}});return}if(i.id===void 0)return;const p=l(c=>f({jsonrpc:"2.0",id:i.id,result:c}),"reply");try{if(i.method==="initialize"){p({protocolVersion:"2024-11-05",capabilities:{tools:{}},serverInfo:{name:"portomatic-slack",version:"0.1.0"}});return}if(i.method==="ping"){p({});return}if(i.method==="tools/list"){p({tools:Object.entries(b).map(([s,n])=>({name:"slack_"+s,description:n.description,inputSchema:{type:"object",properties:n.properties,required:n.required??[],additionalProperties:!1}}))});return}if(i.method!=="tools/call"){f({jsonrpc:"2.0",id:i.id,error:{code:-32601,message:o}});return}const c=i.params?.name?.replace(/^slack_/,""),r=i.params?.arguments??{};if(!b[c])throw new Error(o);for(const s of b[c].required??[])if(r[s]===void 0)throw new Error(o);let a;if(c==="start")await t?.close(),t=await S(r.seed||r.manifest?r:{seed:h}),a={url:t.url,apiUrl:t.apiUrl,ids:t.ids,tokens:t.tokens};else{if(!t)throw new Error(o);const s=r.user?t.as(r.user):void 0,n={channel:t.channel(r.channel??""),ts:r.ts};switch(c){case"seed":await t.control("seed?token_format=structured",r.seed),await t.initialize(),a={ids:t.ids,tokens:t.tokens};break;case"reset":await t.reset(),a={ids:t.ids,tokens:t.tokens};break;case"post":a=await s.post(r.channel,r.text,{thread:r.thread});break;case"react":a=await s.react(n,r.name,{remove:r.remove});break;case"slash":a=await s.slash(r.channel,r.command,r.text);break;case"click":a=await s.clickButton({message:n,actionId:r.actionId});break;case"select":a=await s.selectOption({message:n,actionId:r.actionId,value:r.value});break;case"submit":a=await s.submitView(r.view,r.values);break;case"open_home":a=await s.openHome(r.app);break;case"upload":a=await s.upload(r.channel,r.filename,r.content);break;case"edit":a=await s.edit(n,r.text);break;case"delete":a=await s.delete(n);break;case"history":a=await t.messages(r.channel,r.thread);break;case"deliveries":a=await t.deliveries();break;case"ephemeral":a=await t.ephemeral(r.user);break;case"views":a=await t.views(r.user);break;case"wait":a=await t.waitFor(r.query,y=>Object.entries(r.match??{}).every(([q,g])=>y[q]===g));break;case"idle":await t.idle(r),a={idle:!0};break;case"set_request_url":await t.setRequestUrl(r.url,r.app),a={url:r.url};break;case"close":await t.close(),t=void 0,a={closed:!0};break}}p({content:[{type:"text",text:JSON.stringify(a??{})}]})}catch{p({isError:!0,content:[{type:"text",text:o}]})}}).catch(()=>console.error(o))}),await new Promise(d=>w.once("close",d)),await k,await t?.close(),process.off("SIGINT",m),process.off("SIGTERM",m)}l(O,"serveMcp");export{O as serveMcp};
@@ -1,4 +1,4 @@
1
- var b=Object.defineProperty;var o=(t,e)=>b(t,"name",{value:e,configurable:!0});import{readFile as m,writeFile as g}from"node:fs/promises";import{parse as y}from"yaml";import{readSeed as _}from"../persistence/seed.js";async function d(t){for(const e of t?[t]:["manifest.json","manifest.yaml","manifest.yml"])try{return{path:e,manifest:y(await m(e,"utf8"))}}catch(s){if(t||s.code!=="ENOENT")throw s}}o(d,"findManifest");async function j(t){if(t.seed)return _(t.seed);const e=await d(t.manifest);if(e)return{version:1,users:["alice","bob"],channels:{general:{}},apps:[{name:"bot",manifest:e.manifest,install:{channels:["general"]}}]}}o(j,"projectSeed");async function O(t){const e=await d(t);let s={};try{s=JSON.parse(await m("package.json","utf8"))}catch{}const f=s.dependencies?.["@slack/bolt"]||s.devDependencies?.["@slack/bolt"]?"bolt-js":s.dependencies?.["@slack/web-api"]||s.devDependencies?.["@slack/web-api"]?"slack-sdk":"node",a=await j({manifest:e?.path})??{version:1,users:["alice","bob"],channels:{general:{}},apps:[{name:"bot",scopes:["chat:write","app_mentions:read"],events:["app_mention"],socket_mode:!0,install:{channels:["general"]}}]},u=a.apps[0].manifest?a.apps[0].manifest.settings?.socket_mode_enabled===!0:a.apps[0].socket_mode!==!1,r=a.apps[0],i=(r.events??r.manifest?.settings?.event_subscriptions?.bot_events??[]).includes("app_mention"),k=s.scripts?.start?["npm","start"]:["node",s.main??"app.js"],h=`import { test } from 'node:test';
1
+ var g=Object.defineProperty;var o=(e,s)=>g(e,"name",{value:s,configurable:!0});import{readFile as d,writeFile as y}from"node:fs/promises";import{parse as _}from"yaml";import{readSeed as S}from"../persistence/seed.js";import{USER_ERROR as r}from"../errors.js";async function f(e){for(const s of e?[e]:["manifest.json","manifest.yaml","manifest.yml"])try{return{path:s,manifest:_(await d(s,"utf8"))}}catch(t){if(e||t.code!=="ENOENT")throw new Error(r,{cause:t})}}o(f,"findManifest");async function j(e){if(e.seed)try{return await S(e.seed)}catch(t){throw new Error(r,{cause:t})}const s=await f(e.manifest);if(s)return{version:1,users:["alice","bob"],channels:{general:{}},apps:[{name:"bot",manifest:s.manifest,install:{channels:["general"]}}]}}o(j,"projectSeed");async function $(e){const s=await f(e);let t={};try{t=JSON.parse(await d("package.json","utf8"))}catch{}const u=t.dependencies?.["@slack/bolt"]||t.devDependencies?.["@slack/bolt"]?"bolt-js":t.dependencies?.["@slack/web-api"]||t.devDependencies?.["@slack/web-api"]?"slack-sdk":"node",a=await j({manifest:s?.path})??{version:1,users:["alice","bob"],channels:{general:{}},apps:[{name:"bot",scopes:["chat:write","app_mentions:read"],events:["app_mention"],socket_mode:!0,install:{channels:["general"]}}]},k=a.apps[0].manifest?a.apps[0].manifest.settings?.socket_mode_enabled===!0:a.apps[0].socket_mode!==!1,i=a.apps[0],c=(i.events??i.manifest?.settings?.event_subscriptions?.bot_events??[]).includes("app_mention"),w=t.scripts?.start?["npm","start"]:["node",t.main??"app.js"],h=`import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { startSlack } from 'portomatic/slack/testing';
@@ -6,12 +6,12 @@ import { startSlack } from 'portomatic/slack/testing';
6
6
  test('existing bot replies', async t => {
7
7
  const seed = JSON.parse(await readFile('workspace.json', 'utf8'));
8
8
  const slack = await startSlack({ seed, test: t });
9
- await slack.startBot(${JSON.stringify(k)}${u?"":", { requestUrl: 'http://127.0.0.1:3000/slack/events' }"});
9
+ await slack.startBot(${JSON.stringify(w)}${k?"":", { requestUrl: 'http://127.0.0.1:3000/slack/events' }"});
10
10
  const root = await slack.as('alice').post('general',
11
- ${i?"`<@${slack.ids.apps.bot.bot_user}> hello`":"'hello'"});
11
+ ${c?"`<@${slack.ids.apps.bot.bot_user}> hello`":"'hello'"});
12
12
  // Adapt stimulus and expected text to your existing handlers.
13
- const reply = await slack.waitFor(${i?"{ reply: root }":"{ message: 'general' }, m => m.ts > root.ts && m.user === slack.ids.apps.bot.bot_user"});
13
+ const reply = await slack.waitFor(${c?"{ reply: root }":"{ message: 'general' }, m => m.ts > root.ts && m.user === slack.ids.apps.bot.bot_user"});
14
14
  assert.ok(reply.text);
15
15
  });
16
- `,w=[{path:"workspace.json",content:JSON.stringify(a,null,2)+`
17
- `},{path:"slack.integration.test.mjs",content:h}],c=[],p=[];for(const n of w)try{await g(n.path,n.content,{flag:"wx"}),c.push(n.path)}catch(l){if(l.code!=="EEXIST")throw l;p.push(n.path)}return{sdk:f,manifest:e?.path??null,written:c,preserved:p,command:"node --test slack.integration.test.mjs",installArgs:["npm","i","-D","portomatic"]}}o(O,"initProject");export{d as findManifest,O as initProject,j as projectSeed};
16
+ `,b=[{path:"workspace.json",content:JSON.stringify(a,null,2)+`
17
+ `},{path:"slack.integration.test.mjs",content:h}],p=[],l=[];for(const n of b)try{await y(n.path,n.content,{flag:"wx"}),p.push(n.path)}catch(m){if(m.code!=="EEXIST")throw new Error(r,{cause:m});l.push(n.path)}return{sdk:u,manifest:s?.path??null,written:p,preserved:l,command:"node --test slack.integration.test.mjs",installArgs:["npm","i","-D","portomatic"]}}o($,"initProject");export{f as findManifest,$ as initProject,j as projectSeed};
@@ -1,4 +1,4 @@
1
- var E=Object.defineProperty;var n=(r,s)=>E(r,"name",{value:s,configurable:!0});import{spawn as I}from"node:child_process";import{runtimeEnvironment as R}from"../testing/runtime.js";import{startSlack as k,SlackTest as y}from"../testing/index.js";import{projectSeed as G}from"./project.js";import{followActivity as g,logOptions as M,activityWriter as b}from"./activity.js";import{share as O}from"../share/relay.js";async function N(r,s,h){if(!s.length)throw new Error("Missing command. Example: portomatic slack run -- node app.js");const l=!r.url||r.seed||r.manifest?await G(r):void 0,e=r.url?await new y(r.url).initialize():await k({seed:l,allowSelfEvents:!!r["allow-self-events"]});e.allowSelfEvents=!!r["allow-self-events"],e.allowSelfEvents&&await e.control("self-events",{allowSelfEvents:!0,probe:!1});const i=r.share||process.env.PORTOMATIC_SHARE==="1"?await O(e.url,"slack",t=>process.stderr.write(`share: ${t}
2
- `)).catch(t=>{process.stderr.write(`Share unavailable: ${t.message}
3
- `)}):void 0;let o,f;const c=n(t=>{if(o?.pid)try{process.platform==="win32"?o.kill(t):process.kill(-o.pid,t)}catch(a){if(a.code!=="ESRCH")throw a}},"forward"),d=n(()=>c("SIGINT"),"sigint"),w=n(()=>c("SIGTERM"),"sigterm");try{const t=M(r);f=await g(e.url,t,b(t,p=>process.stderr.write(p))),r.url&&l&&(await e.control("seed?token_format=structured",l),await e.initialize());const a=r["request-url"]??`http://127.0.0.1:${process.env.PORT??process.env.BOT_PORT??3e3}/slack/events`;await e.setRequestUrl(a,r.app);const S={...process.env,...e.env(r.app),...R(),SLACK_EMULATOR_URL:e.url};return h({type:"ready",url:e.url,apiUrl:e.apiUrl,transport:"auto",...i?{dashboard:i.dashboard}:{},next:`portomatic slack doctor --url ${e.url}`}),i&&process.stderr.write(`Dashboard: ${i.dashboard}
4
- `),o=I(s[0],s.slice(1),{env:S,stdio:["inherit","inherit","inherit"],detached:process.platform!=="win32"}),process.once("SIGINT",d),process.once("SIGTERM",w),await new Promise((p,u)=>{o.once("error",u),o.once("exit",(T,m)=>p(T??(m==="SIGINT"?130:m==="SIGTERM"?143:1)))})}finally{process.off("SIGINT",d),process.off("SIGTERM",w),c("SIGTERM"),i?.close();try{await e.close()}finally{await f?.()}}}n(N,"runBot");export{N as runBot};
1
+ var u=Object.defineProperty;var n=(r,o)=>u(r,"name",{value:o,configurable:!0});import{spawn as I}from"node:child_process";import{runtimeEnvironment as k}from"../testing/runtime.js";import{startSlack as y,SlackTest as G}from"../testing/index.js";import{projectSeed as O}from"./project.js";import{followActivity as b,logOptions as M,activityWriter as g}from"./activity.js";import{share as U}from"../share/relay.js";import{dashboardNote as _}from"./help.js";import{USER_ERROR as f}from"../errors.js";async function z(r,o,h){if(!o.length)throw new Error(f);const l=!r.url||r.seed||r.manifest?await O(r):void 0,t=r.url?await new G(r.url).initialize():await y({seed:l,allowSelfEvents:!!r["allow-self-events"]});t.allowSelfEvents=!!r["allow-self-events"],t.allowSelfEvents&&await t.control("self-events",{allowSelfEvents:!0,probe:!1});const i=r.share||process.env.PORTOMATIC_SHARE==="1"?await U(t.url,"slack",()=>process.stderr.write(`${f}
2
+ `)).catch(()=>{process.stderr.write(`${f}
3
+ `)}):void 0;let e,d;const c=n(s=>{if(e?.pid)try{process.platform==="win32"?e.kill(s):process.kill(-e.pid,s)}catch(a){if(a.code!=="ESRCH")throw a}},"forward"),w=n(()=>c("SIGINT"),"sigint"),m=n(()=>c("SIGTERM"),"sigterm");try{const s=M(r);d=await b(t.url,s,g(s,p=>process.stderr.write(p))),r.url&&l&&(await t.control("seed?token_format=structured",l),await t.initialize());const a=r["request-url"]??`http://127.0.0.1:${process.env.PORT??process.env.BOT_PORT??3e3}/slack/events`;await t.setRequestUrl(a,r.app);const R={...process.env,...t.env(r.app),...k(),SLACK_EMULATOR_URL:t.url};return h({type:"ready",url:t.url,apiUrl:t.apiUrl,transport:"auto",...i?{dashboard:i.dashboard,note:_}:{},next:`portomatic slack doctor --url ${t.url}`}),i&&process.stderr.write(`Dashboard: ${i.dashboard}
4
+ `),e=I(o[0],o.slice(1),{env:R,stdio:["inherit","inherit","inherit"],detached:process.platform!=="win32"}),process.once("SIGINT",w),process.once("SIGTERM",m),await new Promise((p,E)=>{e.once("error",E),e.once("exit",(T,S)=>p(T??(S==="SIGINT"?130:S==="SIGTERM"?143:1)))})}finally{process.off("SIGINT",w),process.off("SIGTERM",m),c("SIGTERM"),i?.close();try{await t.close()}finally{await d?.()}}}n(z,"runBot");export{z as runBot};
@@ -0,0 +1 @@
1
+ const o="Error, please contact founders@portomatic.com";export{o as USER_ERROR};
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- var T=Object.defineProperty;var y=(n,e)=>T(n,"name",{value:e,configurable:!0});var O=function(n,e){return typeof n=="string"&&/^\.\.?\//.test(n)?n.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i,function(g,l,u,i,v){return l?e?".jsx":".js":u&&(!i||!v)?g:u+i+"."+v.toLowerCase()+"js"}):n};import{parseArgs as A}from"node:util";import{readFile as N}from"node:fs/promises";import{spawn as C}from"node:child_process";import{Emulator as j}from"./emulator.js";import{SlackTest as I}from"./testing/index.js";import{structuredTokens as R}from"./control/seed.js";import{seedData as P}from"./persistence/seed-commands.js";import{help as x,commands as S}from"./cli/help.js";import{projectSeed as _,initProject as L,findManifest as D}from"./cli/project.js";import{runBot as H}from"./cli/run.js";import{serveMcp as G}from"./cli/mcp.js";import{activityWriter as U,logOptions as q,followActivity as J}from"./cli/activity.js";import{share as K,device as W}from"./share/relay.js";const b=process.argv.slice(2);let p=b.includes("--json"),r="start";const B=y(async n=>import.meta.url.endsWith(".ts")?N(new URL(n==="agents"?"../AGENTS.md":`../docs/${n}.md`,import.meta.url),"utf8"):(await import(O(new URL("../docs/topics.js",import.meta.url).href))).default[n],"readDoc"),M={agents:"Driving the CLI as a coding agent",quickstart:"Install and run",api:"Complete testing API reference",run:"Child environment and SDK redirects",testing:"Lifecycle and assertions","bolt-js":"HTTP and Socket Mode","slack-sdk":"Raw SDK clients",seed:"Manifest and returned IDs","control-plane":"Human actions and observations",troubleshooting:"Errors and fixes"};try{const n=b.indexOf("--"),{values:e,positionals:g}=A({args:n<0?b:b.slice(0,n),allowPositionals:!0,options:{port:{type:"string"},host:{type:"string"},seed:{type:"string"},manifest:{type:"string"},snapshots:{type:"string"},help:{type:"boolean",short:"h"},json:{type:"boolean"},headless:{type:"boolean"},"non-interactive":{type:"boolean"},agent:{type:"string"},endpoint:{type:"string"},url:{type:"string"},app:{type:"string"},"request-url":{type:"string"},"allow-self-events":{type:"boolean"},all:{type:"boolean"},map:{type:"boolean"},verbose:{type:"boolean"},quiet:{type:"boolean",short:"q"},"log-format":{type:"string"},follow:{type:"boolean",short:"f"},share:{type:"boolean"}}});if(p=!!e.json,r=g.shift()??"start",r==="help"){const l=x(g[0],e.all||e.map);console.log(p?JSON.stringify({ok:!0,command:"help",result:l}):l)}else if(e.help){const l=x(b.some(u=>u===r)?r:void 0,e.all||e.map);console.log(p?JSON.stringify({ok:!0,command:"help",result:l}):l)}else{if(!S[r])throw new Error(`Unknown command '${r}'. Use portomatic slack help --all.`);if(p&&["run","mcp"].includes(r))throw new Error(`--json is not supported by ${r}: ${r==="run"?"child stdout is passed through":"stdout is the MCP protocol"}. Remove --json.`);if(e.agent&&!["auto","yes","no"].includes(e.agent))throw new Error("--agent must be auto, yes or no");if(e["log-format"]&&!["human","json"].includes(e["log-format"]))throw new Error("--log-format must be human or json");if(e.follow&&r!=="logs")throw new Error("--follow is supported by logs.");if(e.follow&&p)throw new Error("--json is one envelope; use logs --follow --log-format json for NDJSON.");const l=q(e);if(e.seed&&e.manifest)throw new Error("Choose --seed or --manifest");const u=e.endpoint??e.url??process.env.SLACK_EMULATOR_URL??"http://127.0.0.1:3000",i=y((o,s=S[r][3])=>{p?console.log(JSON.stringify({ok:!0,command:r,result:o,next:`portomatic slack ${s}`})):(console.log(typeof o=="string"?o:JSON.stringify(o,null,2)),console.log(`next: portomatic slack ${s}`))},"emit"),v=y(async()=>{try{const o=await new I(u).initialize();return o.allowSelfEvents=!!e["allow-self-events"],o.allowSelfEvents&&await o.control("self-events",{allowSelfEvents:!0,probe:!1}),o}catch{throw new Error(`Cannot reach Slack at ${u}. Run portomatic slack start, or pass --endpoint <ready-url>.`)}},"attach");if(r==="docs"){const o=g[0];if(o&&!M[o])throw new Error(`Unknown docs topic '${o}'. Use portomatic slack docs.`);i(o?await B(o):Object.entries(M).map(([s,t])=>`${s.padEnd(16)}${t}`).join(`
3
- `),"docs testing")}else if(r==="init")i(await L(e.manifest));else if(r==="dashboard")i({dashboard:(await W()).dashboard_url});else if(r==="mcp")await G(await _(e));else if(r==="run")process.exitCode=await H({...e,url:e.endpoint??e.url},n<0?[]:b.slice(n+1),o=>{l.quiet||U(l,s=>process.stderr.write(s))({...o,v:1,id:0,time:new Date().toISOString(),level:"info",type:"ready",message:`Slack ${o.url}; Web API ${o.apiUrl}`})});else if(r==="env"){let o,s;if(e.endpoint||e.url||process.env.SLACK_EMULATOR_URL)o=await v();else{s=new j({seed:await _(e),seedPath:e.seed??process.env.EMU_SEED}),o=new I(u);const t=P(s.state)?.response;if(!t)throw new Error("Use a version:1 seed");Object.assign(o,R({state:s.state,response:t}))}try{const t=o.env(e.app);i(p?{env:t,ids:o.ids,tokens:o.tokens}:Object.entries(t).map(([d,c])=>`export ${d}='${c.replaceAll("'","'\\''")}'`).join(`
4
- `))}finally{await s?.close()}}else if(r==="start")if(e.endpoint||e.url){const o=await v();i({type:"ready",url:o.url,apiUrl:o.apiUrl,reused:!0})}else{const o=Number(e.port??process.env.EMU_PORT??"3000"),s=Number(process.env.EMU_BODY_LIMIT??1048576);if(!Number.isInteger(o)||o<0||o>65535||!Number.isSafeInteger(s)||s<1)throw new Error("Use --port 0..65535 and a positive EMU_BODY_LIMIT");const t=new j({seed:await _({...e,seed:e.seed??process.env.EMU_SEED}),port:o,bodyLimit:s,host:e.host??process.env.EMU_HOST,snapshotPath:e.snapshots??process.env.EMU_SNAPSHOT_PATH??".emu/snapshots.sqlite"}),d=U(l),c=U(l,a=>process.stderr.write(a)),k=p?()=>{}:t.activity.subscribe(a=>(a.level==="error"?c:d)(a),l);t.onClose(k);try{await t.start(),e["allow-self-events"]&&await new I(t.url).control("self-events",{allowSelfEvents:!0,probe:!1})}catch(a){throw await t.close(),a}const w=y(()=>{t.telemetry.end(),t.close().catch(a=>{console.error(a),process.exitCode=1})},"stop"),f=y(a=>{process.exitCode=1,t.telemetry.crash(a),t.activity.emit("error",{level:"error",message:String(a)}),p&&console.error(t.activity.redact(String(a))),Promise.allSettled([t.telemetry.close(),t.close()]).finally(()=>process.exit(1)),setTimeout(()=>process.exit(1),2500).unref()},"crash");process.once("uncaughtException",f),process.once("unhandledRejection",f),process.once("SIGINT",w),process.once("SIGTERM",w);const h=e.share||process.env.PORTOMATIC_SHARE==="1"?await K(t.url,"slack",a=>t.activity.emit("error",{level:"error",message:a})).catch(a=>{t.activity.emit("error",{level:"error",message:`Share unavailable: ${a.message}`})}):void 0;h&&t.onClose(()=>h.close());const m=h?{dashboard:h.dashboard}:{};p?i({type:"ready",url:t.url,apiUrl:`${t.url}/api/`,pid:process.pid,...m},`doctor --endpoint ${t.url}`):t.activity.emit("ready",{message:`Slack listening at ${t.url}; Web API ${t.url}/api/${h?`; dashboard ${h.dashboard}`:""}`,url:t.url,apiUrl:`${t.url}/api/`,pid:process.pid,...m});const E=e["non-interactive"]||e.agent==="yes"||e.agent!=="no"&&!process.stdout.isTTY||process.env.CI||process.env.SSH_CONNECTION;if(!p&&!E&&!e.headless&&process.env.EMU_HEADLESS!=="1"){const a=process.platform==="darwin"?"open":process.platform==="win32"?"rundll32":"xdg-open",$=`${t.url}/_emu/ui/`;C(a,process.platform==="win32"?["url.dll,FileProtocolHandler",$]:[$],{stdio:"ignore"}).on("error",()=>console.error(`UI: ${$}`))}}else{const o=await v();if(r==="seed"){if(!e.seed)throw new Error("Use seed --seed workspace.json");i(await o.control("seed?token_format=structured",await _(e)))}else if(r==="reset")i(await o.control("reset?token_format=structured",{}));else if(r==="status")i({...await o.control("version"),...await o.control("diagnostics")});else if(r==="stop")i(await o.control("shutdown",{}));else if(r==="logs"&&e.follow){const s=await J(o.url,l);let t;const d=new Promise(c=>{t=c});process.once("SIGINT",t),process.once("SIGTERM",t);try{await Promise.race([s.done,d])}finally{process.off("SIGINT",t),process.off("SIGTERM",t),await s()}}else if(r==="logs"){const{Activity:s}=await import("./activity.js"),t=new s(()=>0),d=await o.control("inspect");t.credentials({tokens:new Map(d.state.tokens),apps:new Map(d.state.apps)});const c=await o.control("calls"),k=await o.deliveries();i({...c,calls:c.calls.map(w=>t.redact(w)),deliveries:k.map(w=>t.redact(w))})}else if(r==="doctor"){const s=await o.control("doctor"),t=await D(e.manifest);if(t){const d=await o.control("inspect"),c=t.manifest,k=c.oauth_config?.scopes?.bot??[],w=c.settings?.event_subscriptions?.bot_events??[];for(const[,f]of d.state.apps){const h=d.state.tokens.filter(([,m])=>m.app_id===f.id&&m.kind==="bot").flatMap(([,m])=>m.scopes);for(const m of k.filter(E=>!h.includes(E)))s.checks.push({app:f.name,check:"project scope",ok:!1,fix:`Add ${m} to oauth_config.scopes.bot in ${t.path}; reseed.`});for(const m of w.filter(E=>!f.bot_events.includes(E)))s.checks.push({app:f.name,check:"project event",ok:!1,fix:`Add ${m} to settings.event_subscriptions.bot_events in the seed manifest; reseed.`});!!c.settings?.socket_mode_enabled!==f.socket_mode&&s.checks.push({app:f.name,check:"transport",ok:!1,fix:`Set settings.socket_mode_enabled to ${!!c.settings?.socket_mode_enabled} in the seeded manifest; reseed.`})}s.healthy=s.checks.every(f=>f.ok)}i(s),s.healthy||(process.exitCode=1)}else if(r==="snapshot"){const[s,t]=g;if(!["save","load","list"].includes(s)||s!=="list"&&!t)throw new Error("Use snapshot save NAME, snapshot load NAME or snapshot list");i(s==="list"?await o.control("snapshots"):await o.control(s==="save"?"snapshot":"restore",{name:t}))}}}}catch(n){const e=n.code==="EADDRINUSE"?"Port busy. Use --port 0 or --endpoint <running-url>.":n.message;p?console.log(JSON.stringify({ok:!1,command:r,error:e,next:`portomatic slack ${S[r]?r:"start"} --help`})):console.error(`${e}
5
- next: portomatic slack ${S[r]?r:"start"} --help`),process.exitCode=1}export{M as topics};
2
+ var A=Object.defineProperty;var E=(a,e)=>A(a,"name",{value:e,configurable:!0});var U=function(a,e){return typeof a=="string"&&/^\.\.?\//.test(a)?a.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i,function(v,l,g,i,y){return l?e?".jsx":".js":g&&(!i||!y)?v:g+i+"."+y.toLowerCase()+"js"}):a};import{parseArgs as N}from"node:util";import{readFile as C}from"node:fs/promises";import{spawn as L}from"node:child_process";import{Emulator as $}from"./emulator.js";import{SlackTest as I}from"./testing/index.js";import{structuredTokens as P}from"./control/seed.js";import{seedData as D}from"./persistence/seed-commands.js";import{help as O,commands as M,dashboardNote as j}from"./cli/help.js";import{projectSeed as _,initProject as H,findManifest as G}from"./cli/project.js";import{runBot as q}from"./cli/run.js";import{serveMcp as J}from"./cli/mcp.js";import{activityWriter as x,logOptions as K,followActivity as W}from"./cli/activity.js";import{share as B,device as F}from"./share/relay.js";import{USER_ERROR as n}from"./errors.js";const S=process.argv.slice(2);let u=S.includes("--json"),r="start";const Y=E(async a=>import.meta.url.endsWith(".ts")?C(new URL(a==="agents"?"../AGENTS.md":`../docs/${a}.md`,import.meta.url),"utf8"):(await import(U(new URL("../docs/topics.js",import.meta.url).href))).default[a],"readDoc"),R={agents:"Driving the CLI as a coding agent",quickstart:"Install and run",api:"Complete testing API reference",run:"Child environment and SDK redirects",testing:"Lifecycle and assertions","bolt-js":"HTTP and Socket Mode","slack-sdk":"Raw SDK clients",seed:"Manifest and returned IDs","control-plane":"Human actions and observations",troubleshooting:"Errors and fixes"};try{const a=S.indexOf("--"),{values:e,positionals:v}=N({args:a<0?S:S.slice(0,a),allowPositionals:!0,options:{port:{type:"string"},host:{type:"string"},seed:{type:"string"},manifest:{type:"string"},snapshots:{type:"string"},help:{type:"boolean",short:"h"},json:{type:"boolean"},headless:{type:"boolean"},"non-interactive":{type:"boolean"},agent:{type:"string"},endpoint:{type:"string"},url:{type:"string"},app:{type:"string"},"request-url":{type:"string"},"allow-self-events":{type:"boolean"},all:{type:"boolean"},map:{type:"boolean"},verbose:{type:"boolean"},quiet:{type:"boolean",short:"q"},"log-format":{type:"string"},follow:{type:"boolean",short:"f"},share:{type:"boolean"}}});if(u=!!e.json,r=v.shift()??"start",r==="help"){const l=O(v[0],e.all||e.map);console.log(u?JSON.stringify({ok:!0,command:"help",result:l}):l)}else if(e.help){const l=O(S.some(g=>g===r)?r:void 0,e.all||e.map);console.log(u?JSON.stringify({ok:!0,command:"help",result:l}):l)}else{if(!M[r])throw new Error(n);if(u&&["run","mcp"].includes(r))throw new Error(n);if(e.agent&&!["auto","yes","no"].includes(e.agent))throw new Error(n);if(e["log-format"]&&!["human","json"].includes(e["log-format"]))throw new Error(n);if(e.follow&&r!=="logs")throw new Error(n);if(e.follow&&u)throw new Error(n);const l=K(e);if(e.seed&&e.manifest)throw new Error(n);const g=e.endpoint??e.url??process.env.SLACK_EMULATOR_URL??"http://127.0.0.1:3000",i=E((o,s=M[r][3])=>{u?console.log(JSON.stringify({ok:!0,command:r,result:o,next:`portomatic slack ${s}`})):(console.log(typeof o=="string"?o:JSON.stringify(o,null,2)),console.log(`next: portomatic slack ${s}`))},"emit"),y=E(async()=>{try{const o=await new I(g).initialize();return o.allowSelfEvents=!!e["allow-self-events"],o.allowSelfEvents&&await o.control("self-events",{allowSelfEvents:!0,probe:!1}),o}catch{throw new Error(n)}},"attach");if(r==="docs"){const o=v[0];if(o&&!R[o])throw new Error(n);i(o?await Y(o):Object.entries(R).map(([s,t])=>`${s.padEnd(16)}${t}`).join(`
3
+ `),"docs testing")}else if(r==="init")i(await H(e.manifest));else if(r==="dashboard")i({dashboard:(await F()).dashboard_url,note:j});else if(r==="mcp")await J(await _(e));else if(r==="run")process.exitCode=await q({...e,url:e.endpoint??e.url},a<0?[]:S.slice(a+1),o=>{l.quiet||x(l,s=>process.stderr.write(s))({...o,v:1,id:0,time:new Date().toISOString(),level:"info",type:"ready",message:`Slack ${o.url}; Web API ${o.apiUrl}`})});else if(r==="env"){let o,s;if(e.endpoint||e.url||process.env.SLACK_EMULATOR_URL)o=await y();else{s=new $({seed:await _(e),seedPath:e.seed??process.env.EMU_SEED}),o=new I(g);const t=D(s.state)?.response;if(!t)throw new Error(n);Object.assign(o,P({state:s.state,response:t}))}try{const t=o.env(e.app);i(u?{env:t,ids:o.ids,tokens:o.tokens}:Object.entries(t).map(([p,c])=>`export ${p}='${c.replaceAll("'","'\\''")}'`).join(`
4
+ `))}finally{await s?.close()}}else if(r==="start")if(e.endpoint||e.url){const o=await y();i({type:"ready",url:o.url,apiUrl:o.apiUrl,reused:!0})}else{const o=Number(e.port??process.env.EMU_PORT??"3000"),s=Number(process.env.EMU_BODY_LIMIT??1048576);if(!Number.isInteger(o)||o<0||o>65535||!Number.isSafeInteger(s)||s<1)throw new Error(n);const t=new $({seed:await _({...e,seed:e.seed??process.env.EMU_SEED}),port:o,bodyLimit:s,host:e.host??process.env.EMU_HOST,snapshotPath:e.snapshots??process.env.EMU_SNAPSHOT_PATH??".emu/snapshots.sqlite"}),p=x(l),c=x(l,m=>process.stderr.write(m)),b=u?()=>{}:t.activity.subscribe(m=>(m.level==="error"?c:p)(m),l);t.onClose(b);try{await t.start(),e["allow-self-events"]&&await new I(t.url).control("self-events",{allowSelfEvents:!0,probe:!1})}catch(m){throw await t.close(),m}const w=E(()=>{t.telemetry.end(),t.close().catch(()=>{console.error(n),process.exitCode=1})},"stop"),d=E(m=>{process.exitCode=1,t.telemetry.crash(m),b(),t.activity.emit("error",{level:"error",message:String(m)}),console.error(n),Promise.allSettled([t.telemetry.close(),t.close()]).finally(()=>process.exit(1)),setTimeout(()=>process.exit(1),2500).unref()},"crash");process.once("uncaughtException",d),process.once("unhandledRejection",d),process.once("SIGINT",w),process.once("SIGTERM",w);const h=e.share||process.env.PORTOMATIC_SHARE==="1"?await B(t.url,"slack",()=>t.activity.emit("error",{level:"error",message:n})).catch(()=>{t.activity.emit("error",{level:"error",message:n})}):void 0;h&&t.onClose(()=>h.close());const f=h?{dashboard:h.dashboard,note:j}:{};u?i({type:"ready",url:t.url,apiUrl:`${t.url}/api/`,pid:process.pid,...f},`doctor --endpoint ${t.url}`):t.activity.emit("ready",{message:`Slack listening at ${t.url}; Web API ${t.url}/api/${h?`; dashboard ${h.dashboard}`:""}`,url:t.url,apiUrl:`${t.url}/api/`,pid:process.pid,...f});const k=e["non-interactive"]||e.agent==="yes"||e.agent!=="no"&&!process.stdout.isTTY||process.env.CI||process.env.SSH_CONNECTION;if(!u&&!k&&!e.headless&&process.env.EMU_HEADLESS!=="1"){const m=process.platform==="darwin"?"open":process.platform==="win32"?"rundll32":"xdg-open",T=`${t.url}/_emu/ui/`;L(m,process.platform==="win32"?["url.dll,FileProtocolHandler",T]:[T],{stdio:"ignore"}).on("error",()=>console.error(n))}}else{const o=await y();if(r==="seed"){if(!e.seed)throw new Error(n);i(await o.control("seed?token_format=structured",await _(e)))}else if(r==="reset")i(await o.control("reset?token_format=structured",{}));else if(r==="status")i({...await o.control("version"),...await o.control("diagnostics")});else if(r==="stop")i(await o.control("shutdown",{}));else if(r==="logs"&&e.follow){const s=await W(o.url,l);let t;const p=new Promise(c=>{t=c});process.once("SIGINT",t),process.once("SIGTERM",t);try{await Promise.race([s.done,p])}finally{process.off("SIGINT",t),process.off("SIGTERM",t),await s()}}else if(r==="logs"){const{Activity:s}=await import("./activity.js"),t=new s(()=>0),p=await o.control("inspect");t.credentials({tokens:new Map(p.state.tokens),apps:new Map(p.state.apps)});const c=await o.control("calls"),b=await o.deliveries();i({...c,calls:c.calls.map(w=>t.redact(w)),deliveries:b.map(w=>t.redact(w))})}else if(r==="doctor"){const s=await o.control("doctor"),t=await G(e.manifest);if(t){const p=await o.control("inspect"),c=t.manifest,b=c.oauth_config?.scopes?.bot??[],w=c.settings?.event_subscriptions?.bot_events??[];for(const[,d]of p.state.apps){const h=p.state.tokens.filter(([,f])=>f.app_id===d.id&&f.kind==="bot").flatMap(([,f])=>f.scopes);for(const f of b.filter(k=>!h.includes(k)))s.checks.push({app:d.name,check:"project scope",ok:!1,fix:`Add ${f} to oauth_config.scopes.bot in ${t.path}; reseed.`});for(const f of w.filter(k=>!d.bot_events.includes(k)))s.checks.push({app:d.name,check:"project event",ok:!1,fix:`Add ${f} to settings.event_subscriptions.bot_events in the seed manifest; reseed.`});!!c.settings?.socket_mode_enabled!==d.socket_mode&&s.checks.push({app:d.name,check:"transport",ok:!1,fix:`Set settings.socket_mode_enabled to ${!!c.settings?.socket_mode_enabled} in the seeded manifest; reseed.`})}s.healthy=s.checks.every(d=>d.ok)}i(s),s.healthy||(process.exitCode=1)}else if(r==="snapshot"){const[s,t]=v;if(!["save","load","list"].includes(s)||s!=="list"&&!t)throw new Error(n);i(s==="list"?await o.control("snapshots"):await o.control(s==="save"?"snapshot":"restore",{name:t}))}}}}catch{u?console.log(JSON.stringify({ok:!1,command:r,error:n})):console.error(n),process.exitCode=1}export{R as topics};
@@ -1,2 +1,2 @@
1
- var H=Object.defineProperty;var p=(c,i)=>H(c,"name",{value:i,configurable:!0});import{createReadStream as x}from"node:fs";import{realpath as d,stat as l}from"node:fs/promises";import{extname as h,resolve as u,sep as w}from"node:path";import{fileURLToPath as y}from"node:url";const j=y(new URL("../../ui/dist/",import.meta.url)),E={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".gif":"image/gif",".webp":"image/webp",".jpg":"image/jpeg",".jpeg":"image/jpeg",".woff":"font/woff",".woff2":"font/woff2",".otf":"font/otf"};function U(c){c.routes.push(async(i,t,n)=>{if(n.pathname!=="/_emu/ui"&&!n.pathname.startsWith("/_emu/ui/"))return!1;if(t.sendDate=!1,t.setHeader("x-content-type-options","nosniff"),i.method!=="GET"&&i.method!=="HEAD")return t.writeHead(405,{allow:"GET, HEAD"}),t.end(),!0;if(n.pathname==="/_emu/ui")return t.writeHead(308,{location:"/_emu/ui/"+n.search}),t.end(),!0;let e;try{e=decodeURIComponent(n.pathname.slice(9))}catch{return t.writeHead(400),t.end(),!0}if(e.includes("\0")||e.includes("\\")||e.split("/").some(a=>a===".."||a.startsWith(".")))return t.writeHead(404),t.end(),!0;const o=u(j),s=u(o,e||"index.html");if(!s.startsWith(o+w))return t.writeHead(404),t.end(),!0;let f=s;try{if(!(await l(f)).isFile())throw new Error("not_file")}catch{if(h(e))return t.writeHead(404),t.end(),!0;f=u(o,"index.html")}try{const a=await d(o),r=await d(f);if(!r.startsWith(a+w))return t.writeHead(404),t.end(),!0;const g=await l(r);if(t.writeHead(200,{"content-type":E[h(r)]??"application/octet-stream","content-length":g.size,"cache-control":"no-cache"}),i.method==="HEAD")t.end();else{const m=x(r);m.on("error",()=>t.destroy()),t.on("close",()=>m.destroy()),m.pipe(t)}}catch{t.writeHead(503,{"content-type":"text/plain; charset=utf-8"}),t.end(`UI build missing. Run npm --prefix ui ci && npm --prefix ui run build.
2
- `)}return!0})}p(U,"install");export{U as install};
1
+ var H=Object.defineProperty;var p=(c,n)=>H(c,"name",{value:n,configurable:!0});import{createReadStream as x}from"node:fs";import{realpath as u,stat as h}from"node:fs/promises";import{extname as l,resolve as s,sep as w}from"node:path";import{fileURLToPath as y}from"node:url";import{USER_ERROR as R}from"../errors.js";const j=y(new URL("../../ui/dist/",import.meta.url)),E={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".gif":"image/gif",".webp":"image/webp",".jpg":"image/jpeg",".jpeg":"image/jpeg",".woff":"font/woff",".woff2":"font/woff2",".otf":"font/otf"};function T(c){c.routes.push(async(n,t,a)=>{if(a.pathname!=="/_emu/ui"&&!a.pathname.startsWith("/_emu/ui/"))return!1;if(t.sendDate=!1,t.setHeader("x-content-type-options","nosniff"),n.method!=="GET"&&n.method!=="HEAD")return t.writeHead(405,{allow:"GET, HEAD"}),t.end(),!0;if(a.pathname==="/_emu/ui")return t.writeHead(308,{location:"/_emu/ui/"+a.search}),t.end(),!0;let e;try{e=decodeURIComponent(a.pathname.slice(9))}catch{return t.writeHead(400),t.end(),!0}if(e.includes("\0")||e.includes("\\")||e.split("/").some(i=>i===".."||i.startsWith(".")))return t.writeHead(404),t.end(),!0;const o=s(j),d=s(o,e||"index.html");if(!d.startsWith(o+w))return t.writeHead(404),t.end(),!0;let f=d;try{if(!(await h(f)).isFile())throw new Error("not_file")}catch{if(l(e))return t.writeHead(404),t.end(),!0;f=s(o,"index.html")}try{const i=await u(o),r=await u(f);if(!r.startsWith(i+w))return t.writeHead(404),t.end(),!0;const g=await h(r);if(t.writeHead(200,{"content-type":E[l(r)]??"application/octet-stream","content-length":g.size,"cache-control":"no-cache"}),n.method==="HEAD")t.end();else{const m=x(r);m.on("error",()=>t.destroy()),t.on("close",()=>m.destroy()),m.pipe(t)}}catch{t.writeHead(503,{"content-type":"text/plain; charset=utf-8"}),t.end(`${R}
2
+ `)}return!0})}p(T,"install");export{T as install};
@@ -1,2 +1,2 @@
1
- var x=Object.defineProperty;var i=(l,g)=>x(l,"name",{value:g,configurable:!0});import y from"ws";import{mkdir as C,readFile as L,writeFile as J}from"node:fs/promises";import{homedir as $}from"node:os";import{join as T}from"node:path";import{randomBytes as F}from"node:crypto";const q=process.env.PORTOMATIC_RELAY_URL??"https://relay.portomatic.com",P=256*1024,H=8*1024*1024,W=new Set(["content-encoding","content-length","transfer-encoding","connection","keep-alive"]),z=i(()=>T(process.env.PORTOMATIC_HOME??T($(),".portomatic"),"device.json"),"deviceFile");async function D(){for(const l of["../../package.json","../../../package.json"])try{return JSON.parse(await L(new URL(l,import.meta.url),"utf8")).version}catch{}return"0.0.0"}i(D,"packageVersion");async function V(){const l=z();try{return JSON.parse(await L(l,"utf8"))}catch{}const g=await fetch(`${q}/v1/devices`,{method:"POST",signal:AbortSignal.timeout(15e3)}),h=await g.json();if(!h.ok)throw new Error(`Portomatic relay signup failed: ${h.error??g.status}`);const{device_id:R,slug:v,device_secret:_,view_key:d,dashboard_url:E}=h,k={device_id:R,slug:v,device_secret:_,view_key:d,dashboard_url:E};return await C(T(l,".."),{recursive:!0,mode:448}),await J(l,JSON.stringify(k,null,2)+`
2
- `,{mode:384}),k}i(V,"device");async function ee(l,g,h=()=>{},R="/_emu/ui/"){const v=await V(),_=process.env.PORTOMATIC_VERSION??await D(),d=new URL(l),E=F(16).toString("hex"),k=[d.origin,`${d.protocol}//localhost:${d.port}`];let f="",w,S=!1,O=1e3,b;const m=new Map,u=new Map,N=i(t=>{if(!f)return t;for(const e of k)t=t.replaceAll(e,f).replaceAll(e.replace(/^http/,"ws"),f.replace(/^https/,"wss"));return t},"rewrite"),A=i(t=>{const e=f&&new URL(f).origin;return f&&t.startsWith(f)?d.origin+t.slice(f.length):e&&t.startsWith(e)?d.origin+t.slice(e.length):t},"localize"),p=i(t=>{w?.readyState===y.OPEN&&w.send(JSON.stringify(t))},"send");async function U(t){const e=m.get(t);m.delete(t);try{const o=new Headers;for(const[c,n]of e.headers)c!=="accept-encoding"&&o.append(c,c==="origin"||c==="referer"?A(n):n);const r=e.body.length&&!["GET","HEAD"].includes(e.method)?Buffer.concat(e.body):void 0,s=await fetch(new URL(e.path,d),{method:e.method,headers:o,body:r,redirect:"manual"}),a=[];s.headers.forEach((c,n)=>{W.has(n)||a.push([n,n==="location"?N(c):c])}),p({t:"res",id:t,status:s.status,headers:a});const M=s.headers.get("content-type")??"";if(/json|text\/(plain|html)/.test(M)&&Number(s.headers.get("content-length")??0)<=H){const c=Buffer.from(N(await s.text()));for(let n=0;n<c.length;n+=P)p({t:"res-body",id:t,b64:c.subarray(n,n+P).toString("base64")})}else if(s.body)for await(const c of s.body)for(let n=0;n<c.length;n+=P)p({t:"res-body",id:t,b64:Buffer.from(c.subarray(n,n+P)).toString("base64")});p({t:"res-end",id:t})}catch(o){p({t:"res-error",id:t,message:String(o.message??o)})}}i(U,"forward");function j(t){const e={origin:d.origin};for(const[s,a]of t.headers)["origin","host","accept-encoding"].includes(s)||(e[s]=a);const o=new y(new URL(t.path,d.origin.replace(/^http/,"ws")),t.protocols,{headers:e}),r={ws:o,queue:[]};u.set(t.sid,r),o.on("open",()=>{for(const s of r.queue)o.send(s);r.queue.length=0}),o.on("message",(s,a)=>p(a?{t:"ws-msg",sid:t.sid,b64:Buffer.from(s).toString("base64")}:{t:"ws-msg",sid:t.sid,text:N(s.toString())})),o.on("close",(s,a)=>{u.delete(t.sid),p({t:"ws-close",sid:t.sid,code:s,reason:a.toString()})}),o.on("error",()=>{})}i(j,"openLocal");function B(t){if(t==="pong")return;const e=JSON.parse(t);switch(e.t){case"hello":f=e.base,O=1e3;break;case"req":m.set(e.id,{method:e.method,path:e.path,headers:e.headers,body:[]});break;case"req-body":m.get(e.id)?.body.push(Buffer.from(e.b64,"base64"));break;case"req-end":m.has(e.id)&&U(e.id);break;case"ws-open":j(e);break;case"ws-msg":{const o=u.get(e.sid);if(!o)break;const r=e.text??Buffer.from(e.b64,"base64");o.ws.readyState===y.OPEN?o.ws.send(r):o.queue.push(r);break}case"ws-close":{const o=u.get(e.sid);u.delete(e.sid),o?.ws.close();break}}}i(B,"onFrame");function I(){if(S)return;const t=new URL("/v1/agent",q.replace(/^http/,"ws"));t.searchParams.set("product",g),t.searchParams.set("token",E),t.searchParams.set("ui",R),t.searchParams.set("port",d.port),t.searchParams.set("version",_);const e=w=new y(t,{headers:{authorization:`Bearer ${v.device_secret}`}});e.on("open",()=>{clearInterval(b),b=setInterval(()=>e.readyState===y.OPEN&&e.send("ping"),25e3),b.unref()}),e.on("message",o=>{try{B(o.toString())}catch(r){h(`relay frame: ${r}`)}}),e.on("unexpected-response",(o,r)=>{if(r.statusCode===426){S=!0;let s="";r.setEncoding("utf8"),r.on("data",a=>{s+=a}),r.on("end",()=>{let a="This Portomatic version is too old for sharing. Update with: npx portomatic@latest";try{a=JSON.parse(s).message||a}catch{}h(a),e.terminate()})}else r.statusCode===401&&h("Portomatic relay rejected this device; delete ~/.portomatic/device.json to register again."),r.resume(),e.terminate()}),e.on("error",()=>{}),e.on("close",()=>{clearInterval(b);for(const{ws:o}of u.values())o.close();u.clear(),m.clear(),!(S||w!==e)&&(setTimeout(I,O).unref(),O=Math.min(O*2,3e4))})}return i(I,"connect"),I(),{dashboard:v.dashboard_url,close(){S=!0,clearInterval(b),w?.close(1e3);for(const{ws:t}of u.values())t.close()}}}i(ee,"share");export{V as device,ee as share};
1
+ var x=Object.defineProperty;var c=(d,y)=>x(d,"name",{value:y,configurable:!0});import b from"ws";import{mkdir as J,readFile as q,writeFile as F}from"node:fs/promises";import{homedir as H}from"node:os";import{join as N}from"node:path";import{randomBytes as W}from"node:crypto";import{USER_ERROR as R}from"../errors.js";const A=process.env.PORTOMATIC_RELAY_URL??"https://relay.portomatic.com",_=256*1024,$=8*1024*1024,z=new Set(["content-encoding","content-length","transfer-encoding","connection","keep-alive"]),D=c(()=>N(process.env.PORTOMATIC_HOME??N(H(),".portomatic"),"device.json"),"deviceFile");async function V(){for(const d of["../../package.json","../../../package.json"])try{return JSON.parse(await q(new URL(d,import.meta.url),"utf8")).version}catch{}return"0.0.0"}c(V,"packageVersion");async function Y(){const d=D();try{return JSON.parse(await q(d,"utf8"))}catch{}const p=await(await fetch(`${A}/v1/devices`,{method:"POST",signal:AbortSignal.timeout(15e3)})).json();if(!p.ok)throw new Error(R);const{device_id:E,slug:v,device_secret:P,view_key:i,dashboard_url:I}=p,k={device_id:E,slug:v,device_secret:P,view_key:i,dashboard_url:I};return await J(N(d,".."),{recursive:!0,mode:448}),await F(d,JSON.stringify(k,null,2)+`
2
+ `,{mode:384}),k}c(Y,"device");async function oe(d,y,p=()=>{},E="/_emu/ui/"){const v=await Y(),P=process.env.PORTOMATIC_VERSION??await V(),i=new URL(d),I=W(16).toString("hex"),k=[i.origin,`${i.protocol}//localhost:${i.port}`];let l="",g,S=!1,O=1e3,w;const m=new Map,f=new Map,L=c(t=>{if(!l)return t;for(const e of k)t=t.replaceAll(e,l).replaceAll(e.replace(/^http/,"ws"),l.replace(/^https/,"wss"));return t},"rewrite"),U=c(t=>{const e=l&&new URL(l).origin;return l&&t.startsWith(l)?i.origin+t.slice(l.length):e&&t.startsWith(e)?i.origin+t.slice(e.length):t},"localize"),h=c(t=>{g?.readyState===b.OPEN&&g.send(JSON.stringify(t))},"send");async function B(t){const e=m.get(t);m.delete(t);try{const o=new Headers;for(const[a,r]of e.headers)a!=="accept-encoding"&&o.append(a,a==="origin"||a==="referer"?U(r):r);const n=e.body.length&&!["GET","HEAD"].includes(e.method)?Buffer.concat(e.body):void 0,s=await fetch(new URL(e.path,i),{method:e.method,headers:o,body:n,redirect:"manual"}),u=[];s.headers.forEach((a,r)=>{z.has(r)||u.push([r,r==="location"?L(a):a])}),h({t:"res",id:t,status:s.status,headers:u});const C=s.headers.get("content-type")??"";if(/json|text\/(plain|html)/.test(C)&&Number(s.headers.get("content-length")??0)<=$){const a=Buffer.from(L(await s.text()));for(let r=0;r<a.length;r+=_)h({t:"res-body",id:t,b64:a.subarray(r,r+_).toString("base64")})}else if(s.body)for await(const a of s.body)for(let r=0;r<a.length;r+=_)h({t:"res-body",id:t,b64:Buffer.from(a.subarray(r,r+_)).toString("base64")});h({t:"res-end",id:t})}catch(o){h({t:"res-error",id:t,message:String(o.message??o)})}}c(B,"forward");function M(t){const e={origin:i.origin};for(const[s,u]of t.headers)["origin","host","accept-encoding"].includes(s)||(e[s]=u);const o=new b(new URL(t.path,i.origin.replace(/^http/,"ws")),t.protocols,{headers:e}),n={ws:o,queue:[]};f.set(t.sid,n),o.on("open",()=>{for(const s of n.queue)o.send(s);n.queue.length=0}),o.on("message",(s,u)=>h(u?{t:"ws-msg",sid:t.sid,b64:Buffer.from(s).toString("base64")}:{t:"ws-msg",sid:t.sid,text:L(s.toString())})),o.on("close",(s,u)=>{f.delete(t.sid),h({t:"ws-close",sid:t.sid,code:s,reason:u.toString()})}),o.on("error",()=>{})}c(M,"openLocal");function j(t){if(t==="pong")return;const e=JSON.parse(t);switch(e.t){case"hello":l=e.base,O=1e3;break;case"req":m.set(e.id,{method:e.method,path:e.path,headers:e.headers,body:[]});break;case"req-body":m.get(e.id)?.body.push(Buffer.from(e.b64,"base64"));break;case"req-end":m.has(e.id)&&B(e.id);break;case"ws-open":M(e);break;case"ws-msg":{const o=f.get(e.sid);if(!o)break;const n=e.text??Buffer.from(e.b64,"base64");o.ws.readyState===b.OPEN?o.ws.send(n):o.queue.push(n);break}case"ws-close":{const o=f.get(e.sid);f.delete(e.sid),o?.ws.close();break}}}c(j,"onFrame");function T(){if(S)return;const t=new URL("/v1/agent",A.replace(/^http/,"ws"));t.searchParams.set("product",y),t.searchParams.set("token",I),t.searchParams.set("ui",E),t.searchParams.set("port",i.port),t.searchParams.set("version",P);const e=g=new b(t,{headers:{authorization:`Bearer ${v.device_secret}`}});e.on("open",()=>{clearInterval(w),w=setInterval(()=>e.readyState===b.OPEN&&e.send("ping"),25e3),w.unref()}),e.on("message",o=>{try{j(o.toString())}catch{p(R)}}),e.on("unexpected-response",(o,n)=>{n.statusCode===426?(S=!0,n.on("end",()=>{p(R),e.terminate()}),n.resume()):(n.statusCode===401&&p(R),n.resume(),e.terminate())}),e.on("error",()=>{}),e.on("close",()=>{clearInterval(w);for(const{ws:o}of f.values())o.close();f.clear(),m.clear(),!(S||g!==e)&&(setTimeout(T,O).unref(),O=Math.min(O*2,3e4))})}return c(T,"connect"),T(),{dashboard:v.dashboard_url,close(){S=!0,clearInterval(w),g?.close(1e3);for(const{ws:t}of f.values())t.close()}}}c(oe,"share");export{Y as device,oe as share};