procdeck 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +84 -19
- package/dist/public.d.ts +7 -0
- package/package.json +1 -1
- package/readme.md +3 -1
- package/schema.json +13 -0
package/dist/cli.js
CHANGED
|
@@ -45446,6 +45446,12 @@ var ProcSpecSchema = Schema_exports.Struct({
|
|
|
45446
45446
|
examples: ["http://localhost:${port}"]
|
|
45447
45447
|
})
|
|
45448
45448
|
),
|
|
45449
|
+
port: Schema_exports.optionalKey(
|
|
45450
|
+
Schema_exports.Number.annotate({
|
|
45451
|
+
description: "Pin this proc's assigned `${port}` to a fixed public number instead of a random free one \u2014 dependents outside the deck (dotenv files, mobile simulators, teammates' scripts) keep their hardcoded addresses, and an observed proc still binds a hidden internal port behind the HTTP observer. Requires `${port}` somewhere in the spec.",
|
|
45452
|
+
examples: [8787]
|
|
45453
|
+
}).pipe(Schema_exports.check(Schema_exports.isInt(), Schema_exports.isBetween({ minimum: 1, maximum: 65535 })))
|
|
45454
|
+
),
|
|
45449
45455
|
needs: Schema_exports.optionalKey(
|
|
45450
45456
|
Schema_exports.Array(Schema_exports.String).annotate({
|
|
45451
45457
|
description: "Ids of procs that must be ready before this one spawns. Declarative: readiness is auto-detected, no port numbers in the config.",
|
|
@@ -45553,6 +45559,25 @@ var validatePortTemplates = (procs) => {
|
|
|
45553
45559
|
}
|
|
45554
45560
|
return void 0;
|
|
45555
45561
|
};
|
|
45562
|
+
var validatePinnedPorts = (config) => {
|
|
45563
|
+
const uiPort = config.port ?? DEFAULT_UI_PORT;
|
|
45564
|
+
const holders = /* @__PURE__ */ new Map();
|
|
45565
|
+
for (const proc of config.procs) {
|
|
45566
|
+
if (proc.port === void 0) continue;
|
|
45567
|
+
if (!usesOwnPort(proc)) {
|
|
45568
|
+
return `proc "${proc.id}" pins port ${proc.port} but does not use \${port} \u2014 the pin is the public side of an assigned port`;
|
|
45569
|
+
}
|
|
45570
|
+
if (proc.port === uiPort) {
|
|
45571
|
+
return `proc "${proc.id}" pins port ${proc.port}, which is the deck UI's own port`;
|
|
45572
|
+
}
|
|
45573
|
+
const holder = holders.get(proc.port);
|
|
45574
|
+
if (holder !== void 0) {
|
|
45575
|
+
return `procs "${holder}" and "${proc.id}" pin the same port ${proc.port}`;
|
|
45576
|
+
}
|
|
45577
|
+
holders.set(proc.port, proc.id);
|
|
45578
|
+
}
|
|
45579
|
+
return void 0;
|
|
45580
|
+
};
|
|
45556
45581
|
var ProcdeckConfigSchema = Schema_exports.Struct({
|
|
45557
45582
|
// JSON configs point at the published schema for editor completion; the key
|
|
45558
45583
|
// is meaningless at runtime but must be accepted, because Struct is exact.
|
|
@@ -45586,7 +45611,7 @@ var ProcdeckConfigSchema = Schema_exports.Struct({
|
|
|
45586
45611
|
}).pipe(
|
|
45587
45612
|
Schema_exports.check(
|
|
45588
45613
|
Schema_exports.makeFilter(
|
|
45589
|
-
(config) => validateGraph(config.procs) ?? validatePortTemplates(config.procs)
|
|
45614
|
+
(config) => validateGraph(config.procs) ?? validatePortTemplates(config.procs) ?? validatePinnedPorts(config)
|
|
45590
45615
|
)
|
|
45591
45616
|
)
|
|
45592
45617
|
).annotate({
|
|
@@ -47320,12 +47345,18 @@ var make63 = Effect_exports.fn("procdeck.supervisor")(function* (loaded) {
|
|
|
47320
47345
|
const live = yield* PubSub_exports.sliding({ capacity: LIVE_BUFFER });
|
|
47321
47346
|
const wantsPort = loaded.config.procs.filter(usesOwnPort);
|
|
47322
47347
|
const observedSpecs = wantsPort.filter((spec) => spec.observe !== false);
|
|
47348
|
+
const unpinned = wantsPort.filter((spec) => spec.port === void 0);
|
|
47323
47349
|
const freePorts = yield* Effect_exports.promise(
|
|
47324
|
-
() => findFreePorts(
|
|
47350
|
+
() => findFreePorts(unpinned.length + observedSpecs.length)
|
|
47325
47351
|
);
|
|
47326
|
-
const assigned = new Map(
|
|
47352
|
+
const assigned = new Map([
|
|
47353
|
+
...wantsPort.flatMap(
|
|
47354
|
+
(spec) => spec.port === void 0 ? [] : [[spec.id, spec.port]]
|
|
47355
|
+
),
|
|
47356
|
+
...unpinned.map((spec, index2) => [spec.id, freePorts[index2]])
|
|
47357
|
+
]);
|
|
47327
47358
|
const internal = new Map(
|
|
47328
|
-
observedSpecs.map((spec, index2) => [spec.id, freePorts[
|
|
47359
|
+
observedSpecs.map((spec, index2) => [spec.id, freePorts[unpinned.length + index2]])
|
|
47329
47360
|
);
|
|
47330
47361
|
const runtimes = /* @__PURE__ */ new Map();
|
|
47331
47362
|
for (const spec of loaded.config.procs) {
|
|
@@ -47340,6 +47371,7 @@ var make63 = Effect_exports.fn("procdeck.supervisor")(function* (loaded) {
|
|
|
47340
47371
|
),
|
|
47341
47372
|
assignedPort,
|
|
47342
47373
|
internalPort,
|
|
47374
|
+
observerClose: void 0,
|
|
47343
47375
|
status: { id: spec.id, state: "stopped" },
|
|
47344
47376
|
proc: void 0,
|
|
47345
47377
|
ready: Latch_exports.makeUnsafe(false),
|
|
@@ -47356,19 +47388,14 @@ var make63 = Effect_exports.fn("procdeck.supervisor")(function* (loaded) {
|
|
|
47356
47388
|
rows: DEFAULT_ROWS
|
|
47357
47389
|
});
|
|
47358
47390
|
}
|
|
47359
|
-
|
|
47360
|
-
|
|
47361
|
-
|
|
47362
|
-
|
|
47363
|
-
() =>
|
|
47364
|
-
|
|
47365
|
-
|
|
47366
|
-
|
|
47367
|
-
})
|
|
47368
|
-
),
|
|
47369
|
-
(close4) => Effect_exports.promise(close4)
|
|
47370
|
-
);
|
|
47371
|
-
}
|
|
47391
|
+
yield* Effect_exports.acquireRelease(
|
|
47392
|
+
Effect_exports.void,
|
|
47393
|
+
() => Effect_exports.promise(
|
|
47394
|
+
() => Promise.all(
|
|
47395
|
+
[...runtimes.values()].map((runtime) => runtime.observerClose?.() ?? Promise.resolve())
|
|
47396
|
+
).then(() => void 0)
|
|
47397
|
+
)
|
|
47398
|
+
);
|
|
47372
47399
|
const emit = (event) => {
|
|
47373
47400
|
PubSub_exports.publishUnsafe(live, event);
|
|
47374
47401
|
};
|
|
@@ -47431,6 +47458,28 @@ var make63 = Effect_exports.fn("procdeck.supervisor")(function* (loaded) {
|
|
|
47431
47458
|
}
|
|
47432
47459
|
}
|
|
47433
47460
|
};
|
|
47461
|
+
const bindObserver = (runtime) => startObserver({
|
|
47462
|
+
publicPort: runtime.assignedPort,
|
|
47463
|
+
internalPort: runtime.internalPort,
|
|
47464
|
+
record: (capture) => runtime.http.record(capture)
|
|
47465
|
+
}).then(
|
|
47466
|
+
(close4) => {
|
|
47467
|
+
runtime.observerClose = close4;
|
|
47468
|
+
return true;
|
|
47469
|
+
},
|
|
47470
|
+
() => false
|
|
47471
|
+
);
|
|
47472
|
+
const parkOnTakenPort = (runtime) => {
|
|
47473
|
+
const hint = `free :${runtime.assignedPort} (is "${runtime.spec.id}" already running outside procdeck?), then Start`;
|
|
47474
|
+
say(
|
|
47475
|
+
runtime,
|
|
47476
|
+
`\r
|
|
47477
|
+
procdeck: pinned port :${runtime.assignedPort} is taken \u2014 cannot observe "${runtime.spec.id}"\r
|
|
47478
|
+
procdeck: hint: ${hint}\r
|
|
47479
|
+
`
|
|
47480
|
+
);
|
|
47481
|
+
setStatus2(runtime, { id: runtime.spec.id, state: "blocked", hint });
|
|
47482
|
+
};
|
|
47434
47483
|
const startSync = (runtime) => {
|
|
47435
47484
|
if (runtime.proc !== void 0) return;
|
|
47436
47485
|
setStatus2(runtime, { id: runtime.spec.id, state: "starting" });
|
|
@@ -47506,6 +47555,13 @@ ${lines2.join("\r\n")}\r
|
|
|
47506
47555
|
const start = Effect_exports.fn("procdeck.start")(function* (id2) {
|
|
47507
47556
|
const runtime = require_(id2);
|
|
47508
47557
|
if (runtime.proc !== void 0 || runtime.waiter !== void 0) return;
|
|
47558
|
+
if (runtime.internalPort !== void 0 && runtime.observerClose === void 0) {
|
|
47559
|
+
const bound = yield* Effect_exports.promise(() => bindObserver(runtime));
|
|
47560
|
+
if (!bound) {
|
|
47561
|
+
parkOnTakenPort(runtime);
|
|
47562
|
+
return;
|
|
47563
|
+
}
|
|
47564
|
+
}
|
|
47509
47565
|
const needs = runtime.spec.needs ?? [];
|
|
47510
47566
|
if (needs.length === 0 && runtime.spec.preflight === void 0) {
|
|
47511
47567
|
return yield* Effect_exports.sync(() => startSync(runtime));
|
|
@@ -47581,7 +47637,9 @@ ${lines2.join("\r\n")}\r
|
|
|
47581
47637
|
list: () => [...runtimes.values()].map((runtime) => ({
|
|
47582
47638
|
id: runtime.spec.id,
|
|
47583
47639
|
command: describeCommand(runtime.resolved),
|
|
47584
|
-
|
|
47640
|
+
// A pinned proc has a stable public address even with no `url` in
|
|
47641
|
+
// the config — derive the link instead of making the user repeat it.
|
|
47642
|
+
...runtime.resolved.url !== void 0 ? { url: runtime.resolved.url } : runtime.spec.port === void 0 ? {} : { url: `http://localhost:${runtime.spec.port}` },
|
|
47585
47643
|
...runtime.assignedPort === void 0 ? {} : { assignedPort: runtime.assignedPort },
|
|
47586
47644
|
proxyUrl: `http://${runtime.spec.id}.localhost:${uiPort}`,
|
|
47587
47645
|
status: runtime.status
|
|
@@ -47636,7 +47694,14 @@ ${lines2.join("\r\n")}\r
|
|
|
47636
47694
|
discard: true
|
|
47637
47695
|
});
|
|
47638
47696
|
for (const runtime of runtimes.values()) {
|
|
47639
|
-
if (runtime.
|
|
47697
|
+
if (runtime.internalPort === void 0) continue;
|
|
47698
|
+
const bound = yield* Effect_exports.promise(() => bindObserver(runtime));
|
|
47699
|
+
if (!bound) parkOnTakenPort(runtime);
|
|
47700
|
+
}
|
|
47701
|
+
for (const runtime of runtimes.values()) {
|
|
47702
|
+
if (runtime.spec.autostart !== false && runtime.status.state !== "blocked") {
|
|
47703
|
+
yield* start(runtime.spec.id);
|
|
47704
|
+
}
|
|
47640
47705
|
}
|
|
47641
47706
|
return { supervisor, stopAll };
|
|
47642
47707
|
});
|
package/dist/public.d.ts
CHANGED
|
@@ -42,6 +42,13 @@ export type ProcSpec = {
|
|
|
42
42
|
readonly autostart?: boolean;
|
|
43
43
|
/** URL this process serves, shown in the UI as a link. */
|
|
44
44
|
readonly url?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Pin this proc's assigned `${port}` to a fixed public number instead of a
|
|
47
|
+
* random free one — dependents outside the deck keep their hardcoded
|
|
48
|
+
* addresses, and an observed proc still binds a hidden internal port.
|
|
49
|
+
* Requires `${port}` somewhere in the spec.
|
|
50
|
+
*/
|
|
51
|
+
readonly port?: number;
|
|
45
52
|
/** Ids of procs that must be ready before this one spawns. */
|
|
46
53
|
readonly needs?: ReadonlyArray<string>;
|
|
47
54
|
/** What "ready" means for dependents. Defaults to "listening". */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "procdeck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Interactive dev-process multiplexer with a web UI: terminal panes in a browser tab, declarative dependencies, assigned ports and a *.localhost reverse proxy.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
package/readme.md
CHANGED
|
@@ -52,7 +52,9 @@ pnpm add -D procdeck # npm i -D procdeck · bun add -d procdeck
|
|
|
52
52
|
- **Ports that never collide.** Write `${port}` in a command, env value or url
|
|
53
53
|
and procdeck assigns a free port before spawning. `${port:api}` is another
|
|
54
54
|
proc's port, so dependents get wired — `API_URL=http://localhost:${port:api}`
|
|
55
|
-
— with no hardcoded numbers anywhere.
|
|
55
|
+
— with no hardcoded numbers anywhere. And when the world already knows the
|
|
56
|
+
address, `port: 8787` pins the public side to that exact number — dotenv
|
|
57
|
+
files and simulators keep working, the observer stays in front.
|
|
56
58
|
- **A memorable address for each service.** `api.localhost:4820`,
|
|
57
59
|
`web.localhost:4820` — whatever port the process actually got. Browsers
|
|
58
60
|
resolve `*.localhost` themselves, so there is zero system setup, and
|
package/schema.json
CHANGED
|
@@ -118,6 +118,19 @@
|
|
|
118
118
|
"http://localhost:${port}"
|
|
119
119
|
]
|
|
120
120
|
},
|
|
121
|
+
"port": {
|
|
122
|
+
"description": "Pin this proc's assigned `${port}` to a fixed public number instead of a random free one — dependents outside the deck (dotenv files, mobile simulators, teammates' scripts) keep their hardcoded addresses, and an observed proc still binds a hidden internal port behind the HTTP observer. Requires `${port}` somewhere in the spec.",
|
|
123
|
+
"examples": [
|
|
124
|
+
8787
|
|
125
|
+
],
|
|
126
|
+
"type": "integer",
|
|
127
|
+
"allOf": [
|
|
128
|
+
{
|
|
129
|
+
"minimum": 1,
|
|
130
|
+
"maximum": 65535
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
121
134
|
"needs": {
|
|
122
135
|
"type": "array",
|
|
123
136
|
"items": {
|