portomatic 0.1.1 → 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 +8 -2
- package/bin/portomatic.mjs +134 -16
- package/package.json +1 -1
- package/products/slack/dist/docs/topics.js +1 -1
- package/products/slack/dist/src/cli/activity.js +5 -5
- package/products/slack/dist/src/cli/help.js +24 -17
- package/products/slack/dist/src/cli/mcp.js +2 -2
- package/products/slack/dist/src/cli/project.js +6 -6
- package/products/slack/dist/src/cli/run.js +4 -4
- package/products/slack/dist/src/errors.js +1 -0
- package/products/slack/dist/src/main.js +3 -4
- package/products/slack/dist/src/packages/ui.js +2 -2
- package/products/slack/dist/src/share/relay.js +2 -2
- package/products/slack/dist/ui/dist/assets/index-R5TsYSq9.js +21 -0
- package/products/slack/dist/ui/dist/index.html +1 -1
- package/products/slack/package.json +1 -1
- package/products/slack/dist/ui/dist/assets/index-CczEfx3Y.js +0 -21
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
|
|
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
|
-
|
|
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.
|
package/bin/portomatic.mjs
CHANGED
|
@@ -2,23 +2,141 @@
|
|
|
2
2
|
// portomatic <product> [command] [flags]: one command for every Portomatic emulator.
|
|
3
3
|
// Each product's CLI runs in this process with its own argv, so output, signals and exit codes are its own.
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
|
-
import {
|
|
5
|
+
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, utimes, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
10
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
+
const version = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
12
|
+
const semver = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\w.-]+))?(?:\+[\w.-]+)?$/;
|
|
13
|
+
function compare(a, b) {
|
|
14
|
+
const x = semver.exec(a), y = semver.exec(b);
|
|
15
|
+
if (!x || !y) return 0;
|
|
16
|
+
for (let i = 1; i <= 3; i++) if (BigInt(x[i]) !== BigInt(y[i])) return BigInt(x[i]) > BigInt(y[i]) ? 1 : -1;
|
|
17
|
+
if (x[4] === y[4]) return 0;
|
|
18
|
+
if (!x[4] || !y[4]) return x[4] ? -1 : 1;
|
|
19
|
+
const p = x[4].split('.'), q = y[4].split('.');
|
|
20
|
+
for (let i = 0; i < Math.max(p.length, q.length); i++) {
|
|
21
|
+
if (p[i] === q[i]) continue;
|
|
22
|
+
if (p[i] === undefined || q[i] === undefined) return p[i] === undefined ? -1 : 1;
|
|
23
|
+
const pn = /^\d+$/.test(p[i]), qn = /^\d+$/.test(q[i]);
|
|
24
|
+
if (pn && qn) { if (BigInt(p[i]) !== BigInt(q[i])) return BigInt(p[i]) > BigInt(q[i]) ? 1 : -1; }
|
|
25
|
+
else return pn !== qn ? (pn ? -1 : 1) : (p[i] > q[i] ? 1 : -1);
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function installed(root, v, directory = join(root, v)) {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = join(directory, 'node_modules/portomatic');
|
|
33
|
+
const metadata = JSON.parse(await readFile(join(pkg, 'package.json'), 'utf8'));
|
|
34
|
+
if (metadata.name === 'portomatic' && metadata.version === v && (await stat(join(pkg, 'bin/portomatic.mjs'))).isFile())
|
|
35
|
+
return { version: v, bin: join(pkg, 'bin/portomatic.mjs') };
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
11
38
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
async function cached(root) {
|
|
40
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
41
|
+
const versions = await Promise.all(entries.filter(e => e.isDirectory() && semver.test(e.name)).map(e => installed(root, e.name)));
|
|
42
|
+
return versions.filter(Boolean).sort((a, b) => compare(b.version, a.version));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function lock(root) {
|
|
46
|
+
const dir = join(root, '.lock'), until = Date.now() + 100_000;
|
|
47
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
48
|
+
while (Date.now() < until) {
|
|
49
|
+
try {
|
|
50
|
+
await mkdir(dir);
|
|
51
|
+
return () => rm(dir, { recursive: true, force: true });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code !== 'EEXIST') throw error;
|
|
54
|
+
const info = await stat(dir).catch(() => undefined);
|
|
55
|
+
if (info && Date.now() - info.mtimeMs > 120_000) await rm(dir, { recursive: true, force: true });
|
|
56
|
+
else await new Promise(resolve => setTimeout(resolve, 100));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new Error('update lock timeout');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function update() {
|
|
63
|
+
const root = join(process.env.PORTOMATIC_HOME ?? join(homedir(), '.portomatic'), 'versions');
|
|
64
|
+
let latest, selected;
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(`${(process.env.PORTOMATIC_REGISTRY ?? 'https://registry.npmjs.org').replace(/\/$/, '')}/portomatic/latest`, { signal: AbortSignal.timeout(1500) });
|
|
67
|
+
if (response.ok) latest = await response.json();
|
|
68
|
+
} catch {}
|
|
69
|
+
try {
|
|
70
|
+
const release = await lock(root);
|
|
71
|
+
try {
|
|
72
|
+
selected = (await cached(root)).find(v => compare(v.version, version) > 0);
|
|
73
|
+
if (semver.test(latest?.version) && compare(latest.version, selected?.version ?? version) > 0 && latest.dist?.tarball) {
|
|
74
|
+
const tmp = await mkdtemp(join(root, '.install-'));
|
|
75
|
+
try {
|
|
76
|
+
console.error(`portomatic: updating ${version} -> ${latest.version}...`);
|
|
77
|
+
await new Promise((resolve, reject) => {
|
|
78
|
+
const child = spawn('npm', ['install', latest.dist.tarball, '--prefix', tmp, '--omit=dev', '--no-save', '--no-audit', '--no-fund', '--loglevel=error'], { stdio: 'ignore' });
|
|
79
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 90_000);
|
|
80
|
+
child.on('error', error => { clearTimeout(timer); reject(error); });
|
|
81
|
+
child.on('close', code => { clearTimeout(timer); code === 0 ? resolve() : reject(new Error('npm install failed')); });
|
|
82
|
+
});
|
|
83
|
+
// Never promote a partial install or a tarball whose version disagrees with the registry.
|
|
84
|
+
if (!await installed(root, latest.version, tmp)) throw new Error('invalid install');
|
|
85
|
+
const target = join(root, latest.version);
|
|
86
|
+
await rm(target, { recursive: true, force: true });
|
|
87
|
+
await rename(tmp, target);
|
|
88
|
+
selected = await installed(root, latest.version);
|
|
89
|
+
if (selected) selected.fresh = true;
|
|
90
|
+
} catch {}
|
|
91
|
+
finally { await rm(tmp, { recursive: true, force: true }); }
|
|
92
|
+
}
|
|
93
|
+
// A long-running emulator still reads UI assets and helpers from its version's directory, so older
|
|
94
|
+
// versions are only removed once no run has used them for a week.
|
|
95
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
96
|
+
for (const e of entries) if (e.isDirectory() && semver.test(e.name) && compare(e.name, selected?.version ?? version) < 0) {
|
|
97
|
+
const used = await stat(join(root, e.name, '.used')).catch(() => undefined);
|
|
98
|
+
if (!used || Date.now() - used.mtimeMs > 7 * 86400_000) await rm(join(root, e.name), { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
} finally { await release(); }
|
|
101
|
+
} catch {}
|
|
102
|
+
return selected ?? (await cached(root)).find(v => compare(v.version, version) > 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.env.PORTOMATIC_VERSION = version;
|
|
106
|
+
let runtime;
|
|
107
|
+
if (process.env.PORTOMATIC_NO_UPDATE !== '1' && process.env.PORTOMATIC_RUNTIME !== '1') {
|
|
108
|
+
try { runtime = await update(); } catch {}
|
|
109
|
+
}
|
|
110
|
+
if (runtime?.fresh) {
|
|
111
|
+
// Updating ends this invocation; the next one runs the new version from the start.
|
|
112
|
+
const message = `portomatic updated ${version} -> ${runtime.version}. Run the command again.`;
|
|
113
|
+
if (process.argv.includes('--json')) console.log(JSON.stringify({ ok: false, error: 'updated', updated: { from: version, to: runtime.version }, message }));
|
|
114
|
+
else console.error(message);
|
|
115
|
+
process.exitCode = 75;
|
|
116
|
+
} else if (runtime) {
|
|
117
|
+
const marker = join(runtime.bin, '../../../../.used'), now = new Date();
|
|
118
|
+
await utimes(marker, now, now).catch(() => writeFile(marker, '')).catch(() => {});
|
|
119
|
+
process.env.PORTOMATIC_RUNTIME = '1';
|
|
120
|
+
process.env.PORTOMATIC_VERSION = runtime.version;
|
|
121
|
+
process.argv = [process.argv[0], runtime.bin, ...process.argv.slice(2)];
|
|
122
|
+
await import(pathToFileURL(runtime.bin).href);
|
|
20
123
|
} else {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
124
|
+
const products = {
|
|
125
|
+
slack: { main: '../products/slack/dist/src/main.js', summary: 'Local Slack: seeded workspaces, bots, the Slack UI and a shareable dashboard' },
|
|
126
|
+
};
|
|
127
|
+
const [product, ...args] = process.argv.slice(2);
|
|
128
|
+
|
|
129
|
+
if (product === '--version' || product === '-v') {
|
|
130
|
+
console.log(version);
|
|
131
|
+
} else if (!products[product]) {
|
|
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.\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
|
+
const asked = !product || ['help', '--help', '-h'].includes(product);
|
|
135
|
+
(asked ? process.stdout : process.stderr).write(asked ? usage : `Error, please contact founders@portomatic.com\n\n${usage}`);
|
|
136
|
+
process.exitCode = asked ? 0 : 1;
|
|
137
|
+
} else {
|
|
138
|
+
const main = new URL(products[product].main, import.meta.url);
|
|
139
|
+
process.argv = [process.argv[0], fileURLToPath(main), ...args];
|
|
140
|
+
await import(main.href);
|
|
141
|
+
}
|
|
24
142
|
}
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var e={agents:"
|
|
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
|
|
2
|
-
`;const
|
|
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
|
|
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
|
|
10
|
-
`).find(
|
|
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=(
|
|
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"]},
|
|
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
|
|
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=
|
|
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 ${
|
|
17
|
+
portomatic slack ${t} ${e[1]}
|
|
12
18
|
|
|
13
19
|
FLAGS
|
|
14
|
-
${
|
|
15
|
-
${
|
|
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(
|
|
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(
|
|
20
|
-
`:""}${
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
51
|
-
`}r(
|
|
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
|
|
2
|
-
`),"send"),
|
|
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
|
|
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(
|
|
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
|
-
${
|
|
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(${
|
|
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
|
-
`,
|
|
17
|
-
`},{path:"slack.integration.test.mjs",content:h}],
|
|
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
|
|
2
|
-
`)).catch(
|
|
3
|
-
`)}):void 0;let
|
|
4
|
-
`),
|
|
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
|
|
3
|
-
`),"docs testing")}else if(r==="init")i(await
|
|
4
|
-
`))}finally{await s?.close()}}else if(r==="start")if(e.endpoint||e.url){const o=await
|
|
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,
|
|
2
|
-
`)}return!0})}p(
|
|
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
|
|
2
|
-
`,{mode:384}),
|
|
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};
|