@protoboardai/cli 0.0.0-stage → 0.2.1
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/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +61 -2
- package/bin/protoboard.mjs +5 -0
- package/build-info.json +8 -0
- package/package.json +54 -4
- package/src/agents.ts +235 -0
- package/src/bom.ts +145 -0
- package/src/dev/build-worker.ts +63 -0
- package/src/dev/layout.ts +199 -0
- package/src/dev/page.ts +284 -0
- package/src/dev/parts.ts +356 -0
- package/src/dev/relay.d.ts +60 -0
- package/src/dev/relay.js +147 -0
- package/src/dev/server.ts +727 -0
- package/src/dev/sources.ts +120 -0
- package/src/gate.ts +102 -0
- package/src/index.ts +2024 -0
- package/src/layout.ts +166 -0
- package/src/mcp/instructions.ts +60 -0
- package/src/mcp/server.ts +215 -0
- package/src/mcp/tools.ts +392 -0
- package/src/openUrl.ts +71 -0
- package/src/setup.ts +298 -0
- package/src/stack-messages.ts +53 -0
- package/src/views-check.ts +267 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the viewer's modules are written down, for its `openSource` event
|
|
3
|
+
* (docs/specs/vscode-extension.md §3): each build of `protoboard dev` writes
|
|
4
|
+
* an index of definition sources and instance declarations next to the
|
|
5
|
+
* bundle, and the server answers `/__protoboard/source` from it, so a host
|
|
6
|
+
* (the VS Code extension) can open the file at the line.
|
|
7
|
+
*
|
|
8
|
+
* - A definition: the file that declares it (a `.uhd.ts` source, or the
|
|
9
|
+
* vendored catalog JSON), at the line of its id.
|
|
10
|
+
* - An instance: the child entry in its parent's definition, at the line of
|
|
11
|
+
* the child's id (searched from the parent's own line, so another module
|
|
12
|
+
* in the same file with a child of that name does not match first; then
|
|
13
|
+
* from the top, for children built from data above the definition).
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
17
|
+
import type { Project } from "@protoboardai/core";
|
|
18
|
+
|
|
19
|
+
export const SOURCES_SCHEMA = "protoboard.dev-sources/v0";
|
|
20
|
+
/** The index's file name, next to the bundle. */
|
|
21
|
+
export const SOURCES_FILE = "sources.json";
|
|
22
|
+
|
|
23
|
+
/** A place in a project file (project-relative). */
|
|
24
|
+
export interface SourcePlace {
|
|
25
|
+
file: string;
|
|
26
|
+
line?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SourceIndex {
|
|
30
|
+
schema: typeof SOURCES_SCHEMA;
|
|
31
|
+
/** Definition id → where it is declared, and its tier (catalog parts are vendored JSON). */
|
|
32
|
+
definitions: Record<string, SourcePlace & { tier: string }>;
|
|
33
|
+
/** Instance path ("arm_fl/motor") → its definition and where its parent declares it. */
|
|
34
|
+
instances: Record<string, SourcePlace & { definition: string }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A resolved place, with the absolute path a host opens. */
|
|
38
|
+
export interface ResolvedPlace extends SourcePlace {
|
|
39
|
+
path: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ResolvedSource {
|
|
43
|
+
defId: string;
|
|
44
|
+
instancePath?: string;
|
|
45
|
+
/** Where the definition is declared. */
|
|
46
|
+
source?: ResolvedPlace;
|
|
47
|
+
/** Where the instance is declared in its parent's definition. */
|
|
48
|
+
instanceSource?: ResolvedPlace;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
|
|
53
|
+
/** The 1-based line of `id: "<value>"` (TypeScript or JSON) in `text`, at or after line `from`. */
|
|
54
|
+
export function lineOf(text: string, value: string, from = 1, key = "id"): number | undefined {
|
|
55
|
+
const needle = new RegExp(`(?:\\b${key}|"${key}")\\s*:\\s*["'\`]${escape(value)}["'\`]`);
|
|
56
|
+
const lines = text.split("\n");
|
|
57
|
+
for (let i = Math.max(0, from - 1); i < lines.length; i++) if (needle.test(lines[i])) return i + 1;
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The index for a loaded project. */
|
|
62
|
+
export function sourceIndex(p: Project): SourceIndex {
|
|
63
|
+
const texts = new Map<string, string | undefined>();
|
|
64
|
+
const text = (file: string) => {
|
|
65
|
+
if (!texts.has(file)) {
|
|
66
|
+
const abs = join(p.root, file);
|
|
67
|
+
texts.set(file, existsSync(abs) ? readFileSync(abs, "utf8") : undefined);
|
|
68
|
+
}
|
|
69
|
+
return texts.get(file);
|
|
70
|
+
};
|
|
71
|
+
const definitions: SourceIndex["definitions"] = {};
|
|
72
|
+
for (const [id, rec] of p.definitions) {
|
|
73
|
+
// a catalog part vendored with its .uhd.ts (PB-910) opens at that source, not the pinned JSON
|
|
74
|
+
const { file, line } = rec.origin.definitionSource ?? rec.origin.source;
|
|
75
|
+
// a vendored catalog file carries no line: the line that names its id
|
|
76
|
+
const content = line ? undefined : text(file);
|
|
77
|
+
const at = line ?? (content === undefined ? undefined : lineOf(content, id));
|
|
78
|
+
definitions[id] = { file, ...(at ? { line: at } : {}), tier: rec.origin.tier };
|
|
79
|
+
}
|
|
80
|
+
const instances: SourceIndex["instances"] = {};
|
|
81
|
+
const definitionAt = new Map(p.instances.map((i) => [i.path, i.definition]));
|
|
82
|
+
for (const inst of p.instances) {
|
|
83
|
+
if (!inst.path) continue;
|
|
84
|
+
const slash = inst.path.lastIndexOf("/");
|
|
85
|
+
const parentPath = slash < 0 ? "" : inst.path.slice(0, slash);
|
|
86
|
+
const childId = inst.path.slice(slash + 1);
|
|
87
|
+
const parentDef = definitionAt.get(parentPath);
|
|
88
|
+
const where = parentDef ? definitions[parentDef] : undefined;
|
|
89
|
+
if (!where) continue;
|
|
90
|
+
const content = text(where.file);
|
|
91
|
+
// after the parent's own line first; children built from data (ARMS.map) are named earlier in the file
|
|
92
|
+
const line = content === undefined ? undefined : (lineOf(content, childId, where.line ?? 1) ?? lineOf(content, childId));
|
|
93
|
+
instances[inst.path] = { definition: inst.definition, file: where.file, ...(line ? { line } : {}) };
|
|
94
|
+
}
|
|
95
|
+
return { schema: SOURCES_SCHEMA, definitions, instances };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Where a definition and an instance are written down. The definition named
|
|
100
|
+
* by `defId` wins over the instance's when both are given; an instance alone
|
|
101
|
+
* also names its definition. Files outside the project are not returned.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveSource(index: SourceIndex, root: string, request: { defId?: string; instancePath?: string }): ResolvedSource | undefined {
|
|
104
|
+
const instancePath = request.instancePath?.replace(/^\/+|\/+$/g, "") || undefined;
|
|
105
|
+
const inst = instancePath ? index.instances[instancePath] : undefined;
|
|
106
|
+
const defId = request.defId ?? inst?.definition;
|
|
107
|
+
if (!defId) return undefined;
|
|
108
|
+
const place = (p?: SourcePlace): ResolvedPlace | undefined => {
|
|
109
|
+
if (!p) return undefined;
|
|
110
|
+
const path = resolve(root, p.file);
|
|
111
|
+
const rel = relative(resolve(root), path);
|
|
112
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return undefined;
|
|
113
|
+
return { file: p.file, ...(p.line ? { line: p.line } : {}), path };
|
|
114
|
+
};
|
|
115
|
+
const def = index.definitions[defId];
|
|
116
|
+
const source = place(def && { file: def.file, line: def.line });
|
|
117
|
+
const instanceSource = place(inst && { file: inst.file, line: inst.line });
|
|
118
|
+
if (!source && !instanceSource) return undefined;
|
|
119
|
+
return { defId, ...(instancePath ? { instancePath } : {}), ...(source ? { source } : {}), ...(instanceSource ? { instanceSource } : {}) };
|
|
120
|
+
}
|
package/src/gate.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which CLI commands need the Protoboard cloud (PB-928, as changed on
|
|
3
|
+
* 2026-10-21): the table the CLI asks before it runs a command.
|
|
4
|
+
*
|
|
5
|
+
* - `local`: works on the project on this machine, with no account and no
|
|
6
|
+
* service. Creating, modelling, checking, building, the viewer, layout,
|
|
7
|
+
* circuits and every other operation, releases, skills, tools, the stack.
|
|
8
|
+
* - `cloud`: uses the library (as a user or a publisher). The library is the Protoboard cloud, and signed out there
|
|
9
|
+
* is no access to it at all: searching it and its
|
|
10
|
+
* category tree, the project's libraries, full revisions and their files,
|
|
11
|
+
* prices and offers, publishing. Signed out, the CLI stops with one line
|
|
12
|
+
* ("… needs a Protoboard account: sign in with `protoboard login`") and
|
|
13
|
+
* exit code 4, before anything changes. The service refuses the same
|
|
14
|
+
* requests on its own: this table only saves a round trip and words the
|
|
15
|
+
* refusal, since a fork can remove it.
|
|
16
|
+
*
|
|
17
|
+
* A library that signs no one in (a local parts service, or another
|
|
18
|
+
* organisation's library read with its own token, `authEnv`) lets a
|
|
19
|
+
* `libraries` command through: requireCloud in index.ts asks each library
|
|
20
|
+
* the command uses.
|
|
21
|
+
*
|
|
22
|
+
* `init` is local; a template's library parts are its one cloud step, and
|
|
23
|
+
* signed out it skips that step and says how to add them later.
|
|
24
|
+
*/
|
|
25
|
+
export type Gate = "local" | "cloud";
|
|
26
|
+
|
|
27
|
+
export interface GateEntry {
|
|
28
|
+
gate: Gate;
|
|
29
|
+
/** For a cloud command: what needs the cloud, as the refusal says it ("Adding library parts"). */
|
|
30
|
+
what?: string;
|
|
31
|
+
/**
|
|
32
|
+
* For a cloud command: which services it uses. `libraries`: the project's
|
|
33
|
+
* libraries (a part can come from any of them; `--library` names one).
|
|
34
|
+
* `cloud`: the project's Protoboard service (prices, offers, publishing).
|
|
35
|
+
*/
|
|
36
|
+
uses?: "libraries" | "cloud";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const LOCAL: GateEntry = { gate: "local" };
|
|
40
|
+
|
|
41
|
+
/** `parts <sub>`: the one command whose subcommands differ. */
|
|
42
|
+
const PARTS: Record<string, GateEntry> = {
|
|
43
|
+
list: LOCAL,
|
|
44
|
+
migrate: LOCAL,
|
|
45
|
+
// browsing is the library too (PB-928, 2026-10-21; it was open before 0.2.0, PB-911)
|
|
46
|
+
search: { gate: "cloud", what: "Searching the Protoboard library", uses: "libraries" },
|
|
47
|
+
taxonomy: { gate: "cloud", what: "Browsing the Protoboard library's categories", uses: "libraries" },
|
|
48
|
+
add: { gate: "cloud", what: "Adding library parts", uses: "libraries" },
|
|
49
|
+
// an update check compares full revisions, which the service serves to signed-in accounts
|
|
50
|
+
update: { gate: "cloud", what: "Updating library parts", uses: "libraries" },
|
|
51
|
+
offers: { gate: "cloud", what: "Fetching offers from the Protoboard library", uses: "cloud" },
|
|
52
|
+
prices: { gate: "cloud", what: "Prices from the Protoboard library", uses: "cloud" },
|
|
53
|
+
publish: { gate: "cloud", what: "Publishing a part", uses: "cloud" },
|
|
54
|
+
usage: { gate: "cloud", what: "The library's usage figures", uses: "cloud" },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Every command, by name; a command not listed is local. */
|
|
58
|
+
export const COMMAND_GATES: Readonly<Record<string, GateEntry>> = {
|
|
59
|
+
init: LOCAL,
|
|
60
|
+
upgrade: LOCAL,
|
|
61
|
+
inspect: LOCAL,
|
|
62
|
+
artifacts: LOCAL,
|
|
63
|
+
check: LOCAL,
|
|
64
|
+
layout: LOCAL,
|
|
65
|
+
build: LOCAL,
|
|
66
|
+
dev: LOCAL,
|
|
67
|
+
bom: LOCAL,
|
|
68
|
+
// by subcommand: commandGate below
|
|
69
|
+
parts: LOCAL,
|
|
70
|
+
login: LOCAL,
|
|
71
|
+
logout: LOCAL,
|
|
72
|
+
whoami: LOCAL,
|
|
73
|
+
// the project's libraries are where its parts come from: with no account, none of them is the Protoboard library's to use
|
|
74
|
+
libraries: { gate: "cloud", what: "Managing the project's libraries", uses: "libraries" },
|
|
75
|
+
doctor: LOCAL,
|
|
76
|
+
tools: LOCAL,
|
|
77
|
+
extensions: LOCAL,
|
|
78
|
+
skills: LOCAL,
|
|
79
|
+
run: LOCAL,
|
|
80
|
+
capabilities: LOCAL,
|
|
81
|
+
release: LOCAL,
|
|
82
|
+
operations: LOCAL,
|
|
83
|
+
evals: LOCAL,
|
|
84
|
+
views: LOCAL,
|
|
85
|
+
stack: LOCAL,
|
|
86
|
+
agents: LOCAL,
|
|
87
|
+
setup: LOCAL,
|
|
88
|
+
mcp: LOCAL,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* What a command line needs. `parts list --check` asks the libraries for
|
|
93
|
+
* updates (full revisions), so it needs the cloud; `parts list` alone does not.
|
|
94
|
+
*/
|
|
95
|
+
export function commandGate(command: string | undefined, sub?: string, flags: { check?: boolean } = {}): GateEntry {
|
|
96
|
+
if (!command) return LOCAL;
|
|
97
|
+
if (command === "parts") {
|
|
98
|
+
if (sub === "list" && flags.check) return { gate: "cloud", what: "Checking library parts for updates", uses: "libraries" };
|
|
99
|
+
return PARTS[sub ?? ""] ?? LOCAL;
|
|
100
|
+
}
|
|
101
|
+
return COMMAND_GATES[command] ?? LOCAL;
|
|
102
|
+
}
|