procdeck 0.2.0 → 0.3.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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The package's public entry point — everything a `procdeck.config.ts` needs.
3
+ *
4
+ * The types are written out structurally instead of being derived from the
5
+ * Effect schema in `config.ts`, because a derived type makes the emitted `.d.ts`
6
+ * import `effect`, and effect is bundled into the CLI rather than installed:
7
+ * consumers would be typechecking against a package they don't have.
8
+ *
9
+ * `tests/public.test.ts` asserts at the type level that this mirrors the schema
10
+ * exactly, so the two cannot drift apart silently.
11
+ */
12
+ /** A check that must pass before the proc spawns; failure parks it in `blocked`. */
13
+ export type Preflight = {
14
+ /** Shell command; exit 0 lets the proc spawn. */
15
+ readonly shell: string;
16
+ /** Regex the check's output must also match, for checks that exit 0 either way. */
17
+ readonly expect?: string;
18
+ /** What the human should do when the check fails. */
19
+ readonly hint?: string;
20
+ };
21
+ /** A regex watched in the pane output; a match raises a badge in the UI. */
22
+ export type Alert = {
23
+ readonly pattern: string;
24
+ /** Badge text. */
25
+ readonly label: string;
26
+ };
27
+ /** A single supervised process. Exactly one of `shell` / `cmd` is used. */
28
+ export type ProcSpec = {
29
+ /** Pane label and proxy subdomain (`api` → http://api.localhost:4820). */
30
+ readonly id: string;
31
+ /** Command line handed to `$SHELL -c`. */
32
+ readonly shell?: string;
33
+ /** argv, executed without a shell. */
34
+ readonly cmd?: ReadonlyArray<string>;
35
+ /** Working directory; defaults to the config file's directory. */
36
+ readonly cwd?: string;
37
+ /** Extra environment; values may use `${port}` / `${port:other}`. */
38
+ readonly env?: {
39
+ readonly [key: string]: string;
40
+ };
41
+ /** Spawn on startup. Defaults to true. */
42
+ readonly autostart?: boolean;
43
+ /** URL this process serves, shown in the UI as a link. */
44
+ readonly url?: string;
45
+ /** Ids of procs that must be ready before this one spawns. */
46
+ readonly needs?: ReadonlyArray<string>;
47
+ /** What "ready" means for dependents. Defaults to "listening". */
48
+ readonly readyWhen?: "listening" | "started";
49
+ readonly preflight?: Preflight;
50
+ readonly alerts?: ReadonlyArray<Alert>;
51
+ };
52
+ /** A procdeck deck: the web UI's settings and the processes it supervises. */
53
+ export type ProcdeckConfig = {
54
+ /** Only meaningful in a JSON config; accepted here for symmetry. */
55
+ readonly $schema?: string;
56
+ /** Deck name; defaults to the config directory's basename. */
57
+ readonly name?: string;
58
+ /** Port for the web UI and the `*.localhost` proxy. Defaults to 4820. */
59
+ readonly port?: number;
60
+ readonly procs: ReadonlyArray<ProcSpec>;
61
+ };
62
+ /** Identity helper that gives config files their types. */
63
+ export declare const defineConfig: (config: ProcdeckConfig) => ProcdeckConfig;
package/dist/public.js ADDED
@@ -0,0 +1,7 @@
1
+ import "./chunk-MLKGABMK.js";
2
+
3
+ // src/public.ts
4
+ var defineConfig = (config) => config;
5
+ export {
6
+ defineConfig
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "procdeck",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
@@ -38,19 +38,19 @@
38
38
  },
39
39
  "exports": {
40
40
  ".": {
41
- "types": "./dist/config.d.ts",
42
- "import": "./dist/config.js"
41
+ "types": "./dist/public.d.ts",
42
+ "import": "./dist/public.js"
43
43
  }
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "effect": "4.0.0-beta.106",
50
- "node-pty": "^1.1.0"
49
+ "@lydell/node-pty": "1.2.0-beta.15"
51
50
  },
52
51
  "devDependencies": {
53
52
  "@effect/platform-browser": "4.0.0-beta.106",
53
+ "effect": "4.0.0-beta.106",
54
54
  "@types/node": "^22.10.1",
55
55
  "@xterm/addon-fit": "^0.11.0",
56
56
  "@xterm/addon-search": "^0.16.0",
package/readme.md CHANGED
@@ -81,14 +81,12 @@ pnpm install
81
81
  pnpm dev # builds the UI, starts the example stack
82
82
  ```
83
83
 
84
- > **Native dependency note.** procdeck runs panes through [node-pty](https://github.com/microsoft/node-pty),
85
- > which ships a native prebuild. npm and yarn set it up automatically. pnpm ≥ 10 and Bun
86
- > block dependency install scripts by default — in your own project run
87
- > `pnpm approve-builds` and allow `node-pty` (Bun: add it to `trustedDependencies`).
88
- > procdeck also repairs the most common fallout by itself at startup (the darwin
89
- > `spawn-helper` prebuild arriving without its execute bit), so approving is only
90
- > strictly required when node-pty must compile from source (no prebuild for your
91
- > platform).
84
+ > **Install footprint.** ~1.8 MB, one dependency. The server is shipped as a bundle, so
85
+ > nothing but the PTY bindings is installed — and those are
86
+ > [`@lydell/node-pty`](https://github.com/lydell/node-pty): the same sources as
87
+ > [node-pty](https://github.com/microsoft/node-pty), distributed as per-platform packages.
88
+ > Nothing compiles and no install script runs, so there is no `pnpm approve-builds` /
89
+ > `trustedDependencies` detour, and no compiler is needed on Linux.
92
90
 
93
91
  Open <http://localhost:4820> for the panes, then <http://web.localhost:4820> and
94
92
  <http://api.localhost:4820> — the example's two servers found each other (and their own
@@ -1,266 +0,0 @@
1
- // src/config.ts
2
- import { existsSync } from "fs";
3
- import { readFile } from "fs/promises";
4
- import { pathToFileURL } from "url";
5
- import * as path from "path";
6
- import { Effect, Schema } from "effect";
7
- var isRegex = Schema.makeFilter((pattern) => {
8
- try {
9
- new RegExp(pattern);
10
- return void 0;
11
- } catch {
12
- return "expected a valid regular expression";
13
- }
14
- });
15
- var regexSource = (annotations) => Schema.String.annotate(annotations).pipe(Schema.check(isRegex));
16
- var PreflightSchema = Schema.Struct({
17
- shell: Schema.String.annotate({
18
- description: "Shell command; exit 0 lets the proc spawn.",
19
- examples: ["wrangler whoami"]
20
- }).pipe(Schema.check(Schema.isMinLength(1))),
21
- expect: Schema.optionalKey(
22
- regexSource({
23
- description: 'Regex the check\'s output must also match. For checks that exit 0 either way \u2014 `wrangler whoami` reports "not authenticated" with exit 0 \u2014 so the config needs no `| grep` pipelines.',
24
- examples: ["You are logged in"]
25
- })
26
- ),
27
- hint: Schema.optionalKey(
28
- Schema.String.annotate({
29
- description: "What the human should do when the check fails; shown on the blocked pane.",
30
- examples: ["run `wrangler login`, then Start"]
31
- })
32
- )
33
- }).annotate({
34
- description: "A gate that must pass before the process spawns \u2014 e.g. `wrangler whoami` before a proc whose tree needs a Cloudflare token. Keeps interactive auth flows out of supervised panes, where a restart would kill their callback servers mid-handshake."
35
- });
36
- var AlertSchema = Schema.Struct({
37
- pattern: regexSource({
38
- description: "Regex matched against a rolling tail of the pane's output.",
39
- examples: ["ERROR", "not authenticated"]
40
- }),
41
- label: Schema.String.annotate({
42
- description: "Badge text shown in the UI when the pattern matches.",
43
- examples: ["needs login"]
44
- }).pipe(Schema.check(Schema.isMinLength(1)))
45
- }).annotate({
46
- description: "A regex watched in the pane output; a match raises a badge in the UI."
47
- });
48
- var ProcSpecSchema = Schema.Struct({
49
- id: Schema.String.annotate({
50
- description: "Stable identifier: the pane label, and the proxy subdomain (`api` \u2192 http://api.localhost:4820).",
51
- examples: ["api", "web"]
52
- }).pipe(Schema.check(Schema.isMinLength(1))),
53
- shell: Schema.optionalKey(
54
- Schema.String.annotate({
55
- description: "Command line handed to `$SHELL -c` \u2014 pipes, `&&` and globs work. Exactly one of `shell` or `cmd` must be set.",
56
- examples: ["pnpm --filter api dev"]
57
- })
58
- ),
59
- cmd: Schema.optionalKey(
60
- Schema.Array(Schema.String).annotate({
61
- description: "argv, executed directly without a shell. Exactly one of `shell` or `cmd` must be set.",
62
- examples: [["node", "server.js"]]
63
- }).pipe(Schema.check(Schema.isMinLength(1)))
64
- ),
65
- cwd: Schema.optionalKey(
66
- Schema.String.annotate({
67
- description: "Working directory; defaults to the config file's directory.",
68
- examples: ["packages/api"]
69
- })
70
- ),
71
- env: Schema.optionalKey(
72
- Schema.Record(Schema.String, Schema.String).annotate({
73
- description: "Extra environment on top of the inherited one. Values may use `${port}` / `${port:other}` templates.",
74
- examples: [{ PORT: "${port}", API_URL: "http://localhost:${port:api}" }]
75
- })
76
- ),
77
- autostart: Schema.optionalKey(
78
- Schema.Boolean.annotate({
79
- description: "Spawn on startup. Defaults to true; false leaves the pane idle until Start."
80
- })
81
- ),
82
- url: Schema.optionalKey(
83
- Schema.String.annotate({
84
- description: "URL this process serves, shown in the UI as a link. Also pins readiness to that URL's port.",
85
- examples: ["http://localhost:${port}"]
86
- })
87
- ),
88
- needs: Schema.optionalKey(
89
- Schema.Array(Schema.String).annotate({
90
- description: "Ids of procs that must be ready before this one spawns. Declarative: readiness is auto-detected, no port numbers in the config.",
91
- examples: [["api"]]
92
- })
93
- ),
94
- readyWhen: Schema.optionalKey(
95
- Schema.Literals(["listening", "started"]).annotate({
96
- description: 'What "ready" means for dependents: "listening" (default) \u2014 the process tree opened a TCP port; "started" \u2014 it merely spawned, for procs that never listen.'
97
- })
98
- ),
99
- preflight: Schema.optionalKey(PreflightSchema),
100
- alerts: Schema.optionalKey(
101
- Schema.Array(AlertSchema).annotate({
102
- description: "Output patterns that raise a badge when they appear in the pane."
103
- })
104
- )
105
- }).pipe(
106
- Schema.check(
107
- Schema.makeFilter(
108
- (spec) => spec.shell === void 0 === (spec.cmd === void 0) ? "expected exactly one of `shell` or `cmd`" : void 0
109
- )
110
- )
111
- ).annotate({
112
- description: "A single supervised process. Exactly one of `shell` / `cmd` is used: `shell` goes through `sh -c` (pipes, `&&`, globs work, at the cost of one extra process in the tree), `cmd` is an argv array executed directly."
113
- });
114
- var DEFAULT_UI_PORT = 4820;
115
- var PORT_TEMPLATE = /\$\{port(?::([^}]+))?\}/g;
116
- var templatedStrings = (spec) => [spec.shell, ...spec.cmd ?? [], ...Object.values(spec.env ?? {}), spec.url].filter(
117
- (text) => text !== void 0
118
- );
119
- var usesOwnPort = (spec) => templatedStrings(spec).some(
120
- (text) => [...text.matchAll(PORT_TEMPLATE)].some((match) => match[1] === void 0)
121
- );
122
- var portRefs = (spec) => templatedStrings(spec).flatMap(
123
- (text) => [...text.matchAll(PORT_TEMPLATE)].flatMap((match) => match[1] === void 0 ? [] : [match[1]])
124
- );
125
- var resolveSpec = (spec, assigned) => {
126
- const sub = (text) => text.replace(PORT_TEMPLATE, (whole, ref) => {
127
- const port = assigned.get(ref ?? spec.id);
128
- return port === void 0 ? whole : String(port);
129
- });
130
- return {
131
- ...spec,
132
- ...spec.shell === void 0 ? {} : { shell: sub(spec.shell) },
133
- ...spec.cmd === void 0 ? {} : { cmd: spec.cmd.map(sub) },
134
- ...spec.env === void 0 ? {} : { env: Object.fromEntries(Object.entries(spec.env).map(([key, value]) => [key, sub(value)])) },
135
- ...spec.url === void 0 ? {} : { url: sub(spec.url) }
136
- };
137
- };
138
- var validateGraph = (procs) => {
139
- const ids = new Set(procs.map((proc) => proc.id));
140
- if (ids.size !== procs.length) return "duplicate proc ids";
141
- for (const proc of procs) {
142
- for (const need of proc.needs ?? []) {
143
- if (!ids.has(need)) return `proc "${proc.id}" needs unknown proc "${need}"`;
144
- }
145
- }
146
- const needsOf = new Map(procs.map((proc) => [proc.id, proc.needs ?? []]));
147
- const done = /* @__PURE__ */ new Set();
148
- const onPath = /* @__PURE__ */ new Set();
149
- const visit = (id) => {
150
- if (done.has(id)) return void 0;
151
- if (onPath.has(id)) return `dependency cycle through "${id}"`;
152
- onPath.add(id);
153
- for (const need of needsOf.get(id) ?? []) {
154
- const cycle = visit(need);
155
- if (cycle !== void 0) return cycle;
156
- }
157
- onPath.delete(id);
158
- done.add(id);
159
- return void 0;
160
- };
161
- for (const proc of procs) {
162
- const cycle = visit(proc.id);
163
- if (cycle !== void 0) return cycle;
164
- }
165
- return void 0;
166
- };
167
- var validatePortTemplates = (procs) => {
168
- const byId = new Map(procs.map((proc) => [proc.id, proc]));
169
- for (const proc of procs) {
170
- for (const ref of portRefs(proc)) {
171
- const target = byId.get(ref);
172
- if (target === void 0) {
173
- return `proc "${proc.id}" references \${port:${ref}} but there is no proc "${ref}"`;
174
- }
175
- if (!usesOwnPort(target)) {
176
- return `proc "${proc.id}" references \${port:${ref}} but "${ref}" does not use \${port} itself`;
177
- }
178
- }
179
- }
180
- return void 0;
181
- };
182
- var ProcdeckConfigSchema = Schema.Struct({
183
- // JSON configs point at the published schema for editor completion; the key
184
- // is meaningless at runtime but must be accepted, because Struct is exact.
185
- $schema: Schema.optionalKey(
186
- Schema.String.annotate({
187
- description: "URL of the procdeck JSON schema, for editor completion and validation.",
188
- examples: ["https://unpkg.com/procdeck/schema.json"]
189
- })
190
- ),
191
- name: Schema.optionalKey(
192
- Schema.String.annotate({
193
- description: "Deck name \u2014 the tab title and the name of the installed web app. Defaults to the config directory's basename.",
194
- examples: ["my-app"]
195
- })
196
- ),
197
- port: Schema.optionalKey(
198
- Schema.Number.annotate({
199
- description: "Port for the web UI and the `*.localhost` proxy. Defaults to 4820.",
200
- examples: [4820]
201
- }).pipe(Schema.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 65535 })))
202
- ),
203
- procs: Schema.Array(ProcSpecSchema).annotate({
204
- description: "The processes this deck supervises."
205
- })
206
- }).pipe(
207
- Schema.check(
208
- Schema.makeFilter(
209
- (config) => validateGraph(config.procs) ?? validatePortTemplates(config.procs)
210
- )
211
- )
212
- ).annotate({
213
- description: "A procdeck deck: the web UI's settings and the processes it supervises."
214
- });
215
- var defineConfig = (config) => config;
216
- var ConfigError = class extends Schema.TaggedError()("ConfigError", {
217
- file: Schema.String,
218
- message: Schema.String
219
- }) {
220
- };
221
- var CONFIG_FILENAMES = [
222
- "procdeck.config.json",
223
- "procdeck.config.ts",
224
- "procdeck.config.js",
225
- "procdeck.config.mjs"
226
- ];
227
- var discoverConfig = (dir) => CONFIG_FILENAMES.map((name) => path.join(dir, name)).find((candidate) => existsSync(candidate));
228
- var readSource = async (absolute) => {
229
- if (path.extname(absolute) === ".json") {
230
- return JSON.parse(await readFile(absolute, "utf8"));
231
- }
232
- const module = await import(pathToFileURL(absolute).href);
233
- return module.default;
234
- };
235
- var loadConfig = Effect.fn("procdeck.loadConfig")(function* (file) {
236
- const absolute = path.resolve(file);
237
- if (!existsSync(absolute)) {
238
- return yield* new ConfigError({ file: absolute, message: "config file not found" });
239
- }
240
- const source = yield* Effect.tryPromise({
241
- try: () => readSource(absolute),
242
- catch: (cause) => new ConfigError({ file: absolute, message: String(cause) })
243
- });
244
- const config = yield* Effect.try({
245
- try: () => Schema.decodeUnknownSync(ProcdeckConfigSchema)(source),
246
- catch: (cause) => new ConfigError({ file: absolute, message: String(cause) })
247
- });
248
- const root = path.dirname(absolute);
249
- const loaded = { config, root, name: config.name ?? path.basename(root) };
250
- return loaded;
251
- });
252
-
253
- export {
254
- PreflightSchema,
255
- AlertSchema,
256
- ProcSpecSchema,
257
- DEFAULT_UI_PORT,
258
- usesOwnPort,
259
- resolveSpec,
260
- ProcdeckConfigSchema,
261
- defineConfig,
262
- ConfigError,
263
- CONFIG_FILENAMES,
264
- discoverConfig,
265
- loadConfig
266
- };
package/dist/config.d.ts DELETED
@@ -1,90 +0,0 @@
1
- import { Effect, Schema } from "effect";
2
- export declare const PreflightSchema: Schema.Struct<{
3
- readonly shell: Schema.String;
4
- readonly expect: Schema.optionalKey<Schema.String>;
5
- readonly hint: Schema.optionalKey<Schema.String>;
6
- }>;
7
- export type Preflight = typeof PreflightSchema.Type;
8
- export declare const AlertSchema: Schema.Struct<{
9
- readonly pattern: Schema.String;
10
- readonly label: Schema.String;
11
- }>;
12
- export type Alert = typeof AlertSchema.Type;
13
- export declare const ProcSpecSchema: Schema.Struct<{
14
- readonly id: Schema.String;
15
- readonly shell: Schema.optionalKey<Schema.String>;
16
- readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
17
- readonly cwd: Schema.optionalKey<Schema.String>;
18
- readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
19
- readonly autostart: Schema.optionalKey<Schema.Boolean>;
20
- readonly url: Schema.optionalKey<Schema.String>;
21
- readonly needs: Schema.optionalKey<Schema.$Array<Schema.String>>;
22
- readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
23
- readonly preflight: Schema.optionalKey<Schema.Struct<{
24
- readonly shell: Schema.String;
25
- readonly expect: Schema.optionalKey<Schema.String>;
26
- readonly hint: Schema.optionalKey<Schema.String>;
27
- }>>;
28
- readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
29
- readonly pattern: Schema.String;
30
- readonly label: Schema.String;
31
- }>>>;
32
- }>;
33
- export type ProcSpec = typeof ProcSpecSchema.Type;
34
- /** Default port of the UI + proxy server. */
35
- export declare const DEFAULT_UI_PORT = 4820;
36
- /** Whether the spec asks procdeck to assign it a port (`${port}` anywhere). */
37
- export declare const usesOwnPort: (spec: ProcSpec) => boolean;
38
- /** Substitute `${port}` / `${port:id}` with the ports assigned at startup. */
39
- export declare const resolveSpec: (spec: ProcSpec, assigned: Map<string, number>) => ProcSpec;
40
- export declare const ProcdeckConfigSchema: Schema.Struct<{
41
- readonly $schema: Schema.optionalKey<Schema.String>;
42
- readonly name: Schema.optionalKey<Schema.String>;
43
- readonly port: Schema.optionalKey<Schema.Number>;
44
- readonly procs: Schema.$Array<Schema.Struct<{
45
- readonly id: Schema.String;
46
- readonly shell: Schema.optionalKey<Schema.String>;
47
- readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
48
- readonly cwd: Schema.optionalKey<Schema.String>;
49
- readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
50
- readonly autostart: Schema.optionalKey<Schema.Boolean>;
51
- readonly url: Schema.optionalKey<Schema.String>;
52
- readonly needs: Schema.optionalKey<Schema.$Array<Schema.String>>;
53
- readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
54
- readonly preflight: Schema.optionalKey<Schema.Struct<{
55
- readonly shell: Schema.String;
56
- readonly expect: Schema.optionalKey<Schema.String>;
57
- readonly hint: Schema.optionalKey<Schema.String>;
58
- }>>;
59
- readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
60
- readonly pattern: Schema.String;
61
- readonly label: Schema.String;
62
- }>>>;
63
- }>>;
64
- }>;
65
- export type ProcdeckConfig = typeof ProcdeckConfigSchema.Type;
66
- /** Identity helper that gives config files their types. */
67
- export declare const defineConfig: (config: ProcdeckConfig) => ProcdeckConfig;
68
- export type LoadedConfig = {
69
- config: ProcdeckConfig;
70
- /** Directory of the config file — the default cwd for every process. */
71
- root: string;
72
- /** `config.name`, or the root directory's basename. */
73
- name: string;
74
- };
75
- declare const ConfigError_base: Schema.Class<ConfigError, Schema.TaggedStruct<"ConfigError", {
76
- readonly file: Schema.String;
77
- readonly message: Schema.String;
78
- }>, import("effect/Cause").YieldableError>;
79
- export declare class ConfigError extends ConfigError_base {
80
- }
81
- /**
82
- * Config file names the CLI looks for when started without an argument, in
83
- * order. JSON comes first because it needs nothing from the toolchain: no
84
- * TypeScript, and no procdeck in the project's dependencies.
85
- */
86
- export declare const CONFIG_FILENAMES: readonly ["procdeck.config.json", "procdeck.config.ts", "procdeck.config.js", "procdeck.config.mjs"];
87
- /** The first config file present in `dir`, if any. */
88
- export declare const discoverConfig: (dir: string) => string | undefined;
89
- export declare const loadConfig: (file: string) => Effect.Effect<LoadedConfig, ConfigError, never>;
90
- export {};
package/dist/config.js DELETED
@@ -1,28 +0,0 @@
1
- import {
2
- AlertSchema,
3
- CONFIG_FILENAMES,
4
- ConfigError,
5
- DEFAULT_UI_PORT,
6
- PreflightSchema,
7
- ProcSpecSchema,
8
- ProcdeckConfigSchema,
9
- defineConfig,
10
- discoverConfig,
11
- loadConfig,
12
- resolveSpec,
13
- usesOwnPort
14
- } from "./chunk-LHJTDX2F.js";
15
- export {
16
- AlertSchema,
17
- CONFIG_FILENAMES,
18
- ConfigError,
19
- DEFAULT_UI_PORT,
20
- PreflightSchema,
21
- ProcSpecSchema,
22
- ProcdeckConfigSchema,
23
- defineConfig,
24
- discoverConfig,
25
- loadConfig,
26
- resolveSpec,
27
- usesOwnPort
28
- };