create-aiaui-module 0.0.0-stage → 0.1.1-TEST
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 +101 -2
- package/bin/create-aiaui-module.js +137 -0
- package/package.json +30 -4
- package/runtime/__init__.py +82 -0
- package/runtime/_runtime/__init__.py +40 -0
- package/runtime/_runtime/host.py +88 -0
- package/runtime/_runtime/loader.py +39 -0
- package/runtime/_runtime/registry.py +93 -0
- package/src/errors.js +6 -0
- package/src/generator.js +194 -0
- package/src/protocol.js +98 -0
- package/templates/HelloWorld/README.md +13 -0
- package/templates/HelloWorld/contractVersion +1 -0
- package/templates/HelloWorld/resources/build.mjs +9 -0
- package/templates/HelloWorld/resources/dist/manifest.json +6 -0
- package/templates/HelloWorld/resources/dist/modules.js +3753 -0
- package/templates/HelloWorld/resources/package.json +12 -0
- package/templates/HelloWorld/resources/src/Hello.ce.vue +25 -0
- package/templates/HelloWorld/resources/src/index.ts +32 -0
- package/templates/HelloWorld/resources/vite.config.mjs +13 -0
- package/templates/JobInformation/CUSTOMIZATION.md +173 -0
- package/templates/JobInformation/README.md +114 -0
- package/templates/JobInformation/api.py +53 -0
- package/templates/JobInformation/contractVersion +1 -0
- package/templates/JobInformation/resources/build.mjs +9 -0
- package/templates/JobInformation/resources/dist/manifest.json +6 -0
- package/templates/JobInformation/resources/dist/modules.js +2919 -0
- package/templates/JobInformation/resources/package-lock.json +1032 -0
- package/templates/JobInformation/resources/package.json +15 -0
- package/templates/JobInformation/resources/src/bus.ts +38 -0
- package/templates/JobInformation/resources/src/contract.ts +74 -0
- package/templates/JobInformation/resources/src/customization.ts +25 -0
- package/templates/JobInformation/resources/src/defaults/DefaultPanel.vue +22 -0
- package/templates/JobInformation/resources/src/defaults/Header.vue +12 -0
- package/templates/JobInformation/resources/src/defaults/InputCarriers.vue +15 -0
- package/templates/JobInformation/resources/src/defaults/OutputCarriers.vue +14 -0
- package/templates/JobInformation/resources/src/defaults/Recipe.vue +18 -0
- package/templates/JobInformation/resources/src/defaults/Timeline.vue +22 -0
- package/templates/JobInformation/resources/src/defaults/slots.ts +16 -0
- package/templates/JobInformation/resources/src/defaults/styles.css +90 -0
- package/templates/JobInformation/resources/src/index.ts +45 -0
- package/templates/JobInformation/resources/src/vite-env.d.ts +1 -0
- package/templates/JobInformation/resources/vite.config.mjs +27 -0
package/src/generator.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
access,
|
|
4
|
+
copyFile,
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
readFile,
|
|
8
|
+
readdir,
|
|
9
|
+
rm,
|
|
10
|
+
} from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
import { GeneratorError } from "./errors.js";
|
|
14
|
+
import { registerModule } from "./protocol.js";
|
|
15
|
+
|
|
16
|
+
export { GeneratorError } from "./errors.js";
|
|
17
|
+
|
|
18
|
+
const MODULE_ID = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
19
|
+
|
|
20
|
+
async function realDirectory(directory) {
|
|
21
|
+
try {
|
|
22
|
+
const entry = await lstat(directory);
|
|
23
|
+
return entry.isDirectory() && !entry.isSymbolicLink();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error.code === "ENOENT") return false;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function templateInfo(root, id) {
|
|
31
|
+
if (!MODULE_ID.test(id)) return null;
|
|
32
|
+
const directory = path.join(root, "templates", id);
|
|
33
|
+
if (!(await realDirectory(directory))) return null;
|
|
34
|
+
try {
|
|
35
|
+
const contractVersion = (
|
|
36
|
+
await readFile(path.join(directory, "contractVersion"), "utf8")
|
|
37
|
+
).trim();
|
|
38
|
+
if (!/^\d+$/.test(contractVersion)) return null;
|
|
39
|
+
return { id, contractVersion, directory };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code === "ENOENT") return null;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function listTemplates(root) {
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = await readdir(path.join(root, "templates"), {
|
|
50
|
+
withFileTypes: true,
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code === "ENOENT")
|
|
54
|
+
throw new GeneratorError(
|
|
55
|
+
"package_missing",
|
|
56
|
+
"Template package is missing",
|
|
57
|
+
);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const templates = await Promise.all(
|
|
61
|
+
entries.map((entry) => templateInfo(root, entry.name)),
|
|
62
|
+
);
|
|
63
|
+
return templates
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map(({ id, contractVersion }) => ({ id, contractVersion }))
|
|
66
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function copyTree(source, target) {
|
|
70
|
+
let filesCopied = 0;
|
|
71
|
+
for (const entry of (await readdir(source, { withFileTypes: true })).sort(
|
|
72
|
+
(a, b) => a.name.localeCompare(b.name),
|
|
73
|
+
)) {
|
|
74
|
+
if (
|
|
75
|
+
["__pycache__", "node_modules", ".git", ".DS_Store"].includes(
|
|
76
|
+
entry.name,
|
|
77
|
+
) ||
|
|
78
|
+
/\.py[co]$/.test(entry.name)
|
|
79
|
+
)
|
|
80
|
+
continue;
|
|
81
|
+
const from = path.join(source, entry.name);
|
|
82
|
+
const to = path.join(target, entry.name);
|
|
83
|
+
if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) {
|
|
84
|
+
throw new GeneratorError(
|
|
85
|
+
"invalid_template",
|
|
86
|
+
`Template contains an unsafe member: ${entry.name}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
await mkdir(to);
|
|
91
|
+
filesCopied += await copyTree(from, to);
|
|
92
|
+
} else {
|
|
93
|
+
await copyFile(from, to, constants.COPYFILE_EXCL);
|
|
94
|
+
filesCopied += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return filesCopied;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function createModule({ templateId, instanceRoot, root }) {
|
|
101
|
+
const template = await templateInfo(root, templateId);
|
|
102
|
+
if (!template)
|
|
103
|
+
throw new GeneratorError(
|
|
104
|
+
"unknown_template",
|
|
105
|
+
`Unknown template: ${templateId}`,
|
|
106
|
+
);
|
|
107
|
+
const instance = path.resolve(instanceRoot);
|
|
108
|
+
if (
|
|
109
|
+
!(await realDirectory(instance)) ||
|
|
110
|
+
!(await realDirectory(path.join(instance, "Config")))
|
|
111
|
+
) {
|
|
112
|
+
throw new GeneratorError(
|
|
113
|
+
"invalid_instance",
|
|
114
|
+
"Instance root must contain a real Config directory",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const modules = path.join(instance, "PyAIAUI");
|
|
118
|
+
if (!(await realDirectory(modules))) {
|
|
119
|
+
try {
|
|
120
|
+
await mkdir(modules);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error.code === "EEXIST") {
|
|
123
|
+
throw new GeneratorError(
|
|
124
|
+
"invalid_instance",
|
|
125
|
+
"PyAIAUI must be a real directory",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
throw new GeneratorError(
|
|
129
|
+
"copy_failed",
|
|
130
|
+
`Could not create PyAIAUI: ${error.message}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await access(modules, constants.W_OK);
|
|
136
|
+
} catch {
|
|
137
|
+
throw new GeneratorError("invalid_instance", "PyAIAUI is not writable");
|
|
138
|
+
}
|
|
139
|
+
const target = path.join(modules, templateId);
|
|
140
|
+
try {
|
|
141
|
+
await mkdir(target);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error.code === "EEXIST")
|
|
144
|
+
throw new GeneratorError("module_exists", `${target} already exists`);
|
|
145
|
+
throw new GeneratorError(
|
|
146
|
+
"copy_failed",
|
|
147
|
+
`Could not create module directory: ${error.message}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const bootstrap = path.join(modules, "__init__.py");
|
|
151
|
+
let createdBootstrap = false;
|
|
152
|
+
let createdRuntime = false;
|
|
153
|
+
try {
|
|
154
|
+
const filesCopied = await copyTree(template.directory, target);
|
|
155
|
+
const runtimeSource = path.join(root, "runtime", "_runtime");
|
|
156
|
+
if (await realDirectory(runtimeSource)) {
|
|
157
|
+
const runtimeTarget = path.join(modules, "_runtime");
|
|
158
|
+
if (!(await realDirectory(runtimeTarget))) {
|
|
159
|
+
await mkdir(runtimeTarget);
|
|
160
|
+
await copyTree(runtimeSource, runtimeTarget);
|
|
161
|
+
createdRuntime = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const bootstrapSource = path.join(root, "runtime", "__init__.py");
|
|
165
|
+
try {
|
|
166
|
+
await copyFile(bootstrapSource, bootstrap, constants.COPYFILE_EXCL);
|
|
167
|
+
createdBootstrap = true;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error.code !== "EEXIST") throw error;
|
|
170
|
+
const existing = await lstat(bootstrap);
|
|
171
|
+
if (!existing.isFile() || existing.isSymbolicLink()) {
|
|
172
|
+
throw new GeneratorError(
|
|
173
|
+
"invalid_instance",
|
|
174
|
+
"PyAIAUI/__init__.py must be a real file",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const registration = await registerModule(instance, templateId);
|
|
179
|
+
return { moduleId: templateId, path: target, filesCopied, ...registration };
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await rm(target, { recursive: true, force: true });
|
|
182
|
+
if (createdBootstrap) await rm(bootstrap, { force: true });
|
|
183
|
+
if (createdRuntime)
|
|
184
|
+
await rm(path.join(modules, "_runtime"), {
|
|
185
|
+
recursive: true,
|
|
186
|
+
force: true,
|
|
187
|
+
});
|
|
188
|
+
if (error instanceof GeneratorError) throw error;
|
|
189
|
+
throw new GeneratorError(
|
|
190
|
+
"copy_failed",
|
|
191
|
+
`Could not copy template: ${error.message}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/protocol.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { lstat, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
import { GeneratorError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
const MODULE_ID = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
8
|
+
|
|
9
|
+
function validProtocol(value) {
|
|
10
|
+
return (
|
|
11
|
+
value &&
|
|
12
|
+
typeof value === "object" &&
|
|
13
|
+
!Array.isArray(value) &&
|
|
14
|
+
value.schemaVersion === 1 &&
|
|
15
|
+
(value.port === null ||
|
|
16
|
+
(Number.isInteger(value.port) &&
|
|
17
|
+
value.port >= 1 &&
|
|
18
|
+
value.port <= 65535)) &&
|
|
19
|
+
Array.isArray(value.modules) &&
|
|
20
|
+
value.modules.every(
|
|
21
|
+
(module) =>
|
|
22
|
+
module &&
|
|
23
|
+
typeof module === "object" &&
|
|
24
|
+
MODULE_ID.test(module.name) &&
|
|
25
|
+
typeof module.enabled === "boolean",
|
|
26
|
+
) &&
|
|
27
|
+
new Set(value.modules.map((module) => module.name)).size ===
|
|
28
|
+
value.modules.length
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function registerModule(instanceRoot, moduleId) {
|
|
33
|
+
const configPath = path.join(instanceRoot, "Config", "AIAUIProtocol.json");
|
|
34
|
+
const draft = {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
port: null,
|
|
37
|
+
modules: [{ name: moduleId, enabled: false }],
|
|
38
|
+
};
|
|
39
|
+
let entry;
|
|
40
|
+
try {
|
|
41
|
+
entry = await lstat(configPath);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.code !== "ENOENT") throw error;
|
|
44
|
+
}
|
|
45
|
+
if (!entry) {
|
|
46
|
+
try {
|
|
47
|
+
const handle = await open(configPath, "wx");
|
|
48
|
+
try {
|
|
49
|
+
await handle.writeFile(JSON.stringify(draft, null, 2) + "\n");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
await handle.close();
|
|
52
|
+
await rm(configPath, { force: true });
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
await handle.close();
|
|
56
|
+
return { configPath, enabled: false, port: null };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.code !== "EEXIST") throw error;
|
|
59
|
+
entry = await lstat(configPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
63
|
+
throw new GeneratorError(
|
|
64
|
+
"invalid_protocol",
|
|
65
|
+
"AIAUIProtocol.json must be a real file",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
let protocol;
|
|
69
|
+
try {
|
|
70
|
+
protocol = JSON.parse(await readFile(configPath, "utf8"));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new GeneratorError(
|
|
73
|
+
"invalid_protocol",
|
|
74
|
+
`AIAUIProtocol.json is not valid JSON: ${error.message}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (!validProtocol(protocol)) {
|
|
78
|
+
throw new GeneratorError(
|
|
79
|
+
"invalid_protocol",
|
|
80
|
+
"AIAUIProtocol.json must have schemaVersion 1, a port or null, and a modules array",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const existing = protocol.modules.find((module) => module.name === moduleId);
|
|
84
|
+
if (existing)
|
|
85
|
+
return { configPath, enabled: existing.enabled, port: protocol.port };
|
|
86
|
+
protocol.modules.push({ name: moduleId, enabled: false });
|
|
87
|
+
const temporary = `${configPath}.${randomUUID()}.tmp`;
|
|
88
|
+
try {
|
|
89
|
+
await writeFile(temporary, JSON.stringify(protocol, null, 2) + "\n", {
|
|
90
|
+
flag: "wx",
|
|
91
|
+
mode: entry.mode,
|
|
92
|
+
});
|
|
93
|
+
await rename(temporary, configPath);
|
|
94
|
+
} finally {
|
|
95
|
+
await rm(temporary, { force: true });
|
|
96
|
+
}
|
|
97
|
+
return { configPath, enabled: false, port: protocol.port };
|
|
98
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# HelloWorld module
|
|
2
|
+
|
|
3
|
+
This frontend-only template shows a minimal AIAUI module. Edit `resources/src/Hello.ce.vue`, then run `npm install` and `npm run build` inside `resources/` to refresh the prebuilt file. `resources/src/index.ts` remains the module entry. There is intentionally no `api.py`.
|
|
4
|
+
|
|
5
|
+
Generation creates `PyAIAUI/HelloWorld/` and registers it as disabled in `Config/AIAUIProtocol.json`. Set a port and enable it after integrating the module host:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from PyAIAUI import AIAUI
|
|
9
|
+
|
|
10
|
+
aiaui = AIAUI.start(services={...})
|
|
11
|
+
# ... later ...
|
|
12
|
+
aiaui.stop()
|
|
13
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1
|