@appstrata/cli 0.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/README.md +304 -0
- package/dist/commands/dev-http-player.d.ts +23 -0
- package/dist/commands/dev-http-player.d.ts.map +1 -0
- package/dist/commands/dev-http-player.js +360 -0
- package/dist/commands/dev.d.ts +21 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +114 -0
- package/dist/commands/package.d.ts +18 -0
- package/dist/commands/package.d.ts.map +1 -0
- package/dist/commands/package.js +161 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/runtimes/index.d.ts +19 -0
- package/dist/runtimes/index.d.ts.map +1 -0
- package/dist/runtimes/index.js +29 -0
- package/dist/runtimes/python.d.ts +13 -0
- package/dist/runtimes/python.d.ts.map +1 -0
- package/dist/runtimes/python.js +120 -0
- package/dist/runtimes/types.d.ts +74 -0
- package/dist/runtimes/types.d.ts.map +1 -0
- package/dist/runtimes/types.js +8 -0
- package/dist/schema-generator.d.ts +41 -0
- package/dist/schema-generator.d.ts.map +1 -0
- package/dist/schema-generator.js +239 -0
- package/dist/status-page.d.ts +5 -0
- package/dist/status-page.d.ts.map +1 -0
- package/dist/status-page.js +71 -0
- package/package.json +50 -0
- package/python/appstrata_dev_player/__init__.py +1 -0
- package/python/appstrata_dev_player/__main__.py +102 -0
- package/python/appstrata_dev_player/config.py +124 -0
- package/python/appstrata_dev_player/server.py +508 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appstrata dev` command - Start development player with iframe.
|
|
3
|
+
*
|
|
4
|
+
* Loads the user's app in an iframe and provides a mock player environment.
|
|
5
|
+
* Optionally relays messages to a remote HTTP player instead of using a
|
|
6
|
+
* local mock host.
|
|
7
|
+
*/
|
|
8
|
+
interface DevOptions {
|
|
9
|
+
url: string;
|
|
10
|
+
port: string;
|
|
11
|
+
config: string;
|
|
12
|
+
host?: boolean;
|
|
13
|
+
relay?: string;
|
|
14
|
+
transport?: "sse" | "polling";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Start the development player.
|
|
18
|
+
*/
|
|
19
|
+
export declare function devCommand(options: DevOptions): Promise<void>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=dev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,UAAU,UAAU;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;CAC/B;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAiHnE"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appstrata dev` command - Start development player with iframe.
|
|
3
|
+
*
|
|
4
|
+
* Loads the user's app in an iframe and provides a mock player environment.
|
|
5
|
+
* Optionally relays messages to a remote HTTP player instead of using a
|
|
6
|
+
* local mock host.
|
|
7
|
+
*/
|
|
8
|
+
import { createServer } from "vite";
|
|
9
|
+
import { appstrata, loadConfig } from "@appstrata/dev";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
/**
|
|
13
|
+
* Start the development player.
|
|
14
|
+
*/
|
|
15
|
+
export async function devCommand(options) {
|
|
16
|
+
const appUrl = options.url;
|
|
17
|
+
const isRelay = !!options.relay;
|
|
18
|
+
// Resolve the config to find the actual file for
|
|
19
|
+
// the banner. The plugin will load it again in configureServer.
|
|
20
|
+
const result = await loadConfig(options.config);
|
|
21
|
+
const configPath = result?.filePath ?? path.resolve(process.cwd(), options.config);
|
|
22
|
+
const configName = path.basename(configPath);
|
|
23
|
+
console.log("\n Starting AppStrata Dev Player...\n");
|
|
24
|
+
console.log(` App URL: ${appUrl}`);
|
|
25
|
+
console.log(` Config file: ${configName}`);
|
|
26
|
+
console.log(` Port: ${options.port}`);
|
|
27
|
+
console.log(` Host: ${options.host ? "true" : "false"}`);
|
|
28
|
+
if (isRelay) {
|
|
29
|
+
console.log(` Relay to: ${options.relay}`);
|
|
30
|
+
console.log(` Transport: ${options.transport ?? "sse"}`);
|
|
31
|
+
}
|
|
32
|
+
console.log("\n Make sure your app dev server is running!\n");
|
|
33
|
+
try {
|
|
34
|
+
// Resolve path to player page from @appstrata/dev
|
|
35
|
+
// When running from CLI, we need to find the player directory in @appstrata/dev
|
|
36
|
+
const devPackagePath = path.dirname(fileURLToPath(import.meta.resolve("@appstrata/dev")));
|
|
37
|
+
// Check if we're in development (workspace) or production (npm installed)
|
|
38
|
+
// In dev: devPackagePath = .../packages/dev/dist
|
|
39
|
+
// In prod: devPackagePath = .../node_modules/@appstrata/dev/dist
|
|
40
|
+
const isWorkspace = devPackagePath.includes("packages") && !devPackagePath.includes("node_modules");
|
|
41
|
+
// Use source directory in workspace, dist in production
|
|
42
|
+
const playerRoot = isWorkspace
|
|
43
|
+
? path.join(devPackagePath, "..", "src", "player") // dev: use src
|
|
44
|
+
: path.join(devPackagePath, "player"); // prod: use dist
|
|
45
|
+
console.log(`[Debug] Player root: ${playerRoot}`);
|
|
46
|
+
console.log(`[Debug] Workspace mode: ${isWorkspace}`);
|
|
47
|
+
console.log(`[Debug] Config path (resolved): ${configPath}`);
|
|
48
|
+
// Build plugin options, including relay config if provided
|
|
49
|
+
const pluginOptions = {
|
|
50
|
+
configFile: configPath,
|
|
51
|
+
player: "standalone",
|
|
52
|
+
};
|
|
53
|
+
if (options.relay) {
|
|
54
|
+
pluginOptions.relay = {
|
|
55
|
+
playerUrl: options.relay,
|
|
56
|
+
transport: options.transport ?? "sse",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const server = await createServer({
|
|
60
|
+
root: playerRoot,
|
|
61
|
+
plugins: [
|
|
62
|
+
appstrata(pluginOptions),
|
|
63
|
+
],
|
|
64
|
+
server: {
|
|
65
|
+
port: parseInt(options.port, 10),
|
|
66
|
+
host: options.host,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
await server.listen();
|
|
70
|
+
const address = server.httpServer?.address();
|
|
71
|
+
const port = typeof address === "object" && address ? address.port : options.port;
|
|
72
|
+
const hostOrigin = `http://localhost:${port}`;
|
|
73
|
+
// Open browser with dynamic hostOrigin
|
|
74
|
+
const appUrlWithHost = `${appUrl}?hostOrigin=${encodeURIComponent(hostOrigin)}`;
|
|
75
|
+
const playerUrl = `/?appUrl=${encodeURIComponent(appUrlWithHost)}`;
|
|
76
|
+
const fullUrl = `http://localhost:${port}${playerUrl}`;
|
|
77
|
+
console.log(" AppStrata Dev Player running\n");
|
|
78
|
+
console.log(` ➜ Local: ${fullUrl}`);
|
|
79
|
+
if (options.host) {
|
|
80
|
+
// Get network addresses
|
|
81
|
+
const { networkInterfaces } = await import("node:os");
|
|
82
|
+
const nets = networkInterfaces();
|
|
83
|
+
for (const name of Object.keys(nets)) {
|
|
84
|
+
for (const net of nets[name] || []) {
|
|
85
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
86
|
+
console.log(` ➜ Network: http://${net.address}:${port}/?appUrl=${encodeURIComponent(appUrl)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (isRelay) {
|
|
92
|
+
console.log("\n Relay features:");
|
|
93
|
+
console.log(" • PostMessage transport to app (iframe)");
|
|
94
|
+
console.log(` • HTTP transport to remote player (${options.transport ?? "sse"})`);
|
|
95
|
+
console.log(" • Real-time message statistics");
|
|
96
|
+
console.log(" • HMR for player UI settings");
|
|
97
|
+
console.log(`\n Edit ${configName} to change player UI (HMR supported).`);
|
|
98
|
+
console.log(" App context is managed by the remote player.\n");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log("\n Player features:");
|
|
102
|
+
console.log(" • Message protocol (like production web players)");
|
|
103
|
+
console.log(" • Lifecycle events (onInit, onShow, onStart)");
|
|
104
|
+
console.log(" • Mock capability APIs");
|
|
105
|
+
console.log(" • HMR for config changes");
|
|
106
|
+
console.log(`\n Edit ${configName} to change mock context & player UI (HMR supported)\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error("\n Failed to start player:\n");
|
|
111
|
+
console.error(error);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appstrata package` command - Package app into a zip bundle for CMS deployment.
|
|
3
|
+
*
|
|
4
|
+
* Inputs (convention-based, resolved from CWD):
|
|
5
|
+
* - appstrata.config.ts (flag: -c)
|
|
6
|
+
* - dist/ (flag: -o)
|
|
7
|
+
* - .appstrata/package/ (convention, no flag)
|
|
8
|
+
*
|
|
9
|
+
* Output:
|
|
10
|
+
* - dist/<app-id>.zip
|
|
11
|
+
*/
|
|
12
|
+
interface PackageOptions {
|
|
13
|
+
config: string;
|
|
14
|
+
outDir: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function packageCommand(options: PackageOptions): Promise<void>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=package.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../../src/commands/package.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AA4JD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAoD3E"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appstrata package` command - Package app into a zip bundle for CMS deployment.
|
|
3
|
+
*
|
|
4
|
+
* Inputs (convention-based, resolved from CWD):
|
|
5
|
+
* - appstrata.config.ts (flag: -c)
|
|
6
|
+
* - dist/ (flag: -o)
|
|
7
|
+
* - .appstrata/package/ (convention, no flag)
|
|
8
|
+
*
|
|
9
|
+
* Output:
|
|
10
|
+
* - dist/<app-id>.zip
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import archiver from "archiver";
|
|
15
|
+
import { loadConfig } from "@appstrata/dev";
|
|
16
|
+
import { generateYodeckSchema } from "../schema-generator.js";
|
|
17
|
+
const PACKAGE_DIR = ".appstrata/package";
|
|
18
|
+
function validateBuildOutput(distDir) {
|
|
19
|
+
if (!fs.existsSync(distDir)) {
|
|
20
|
+
throw new Error(`Build output directory not found: ${distDir}\n` +
|
|
21
|
+
` Run your build command first (e.g., "npm run build").`);
|
|
22
|
+
}
|
|
23
|
+
const indexPath = path.join(distDir, "index.html");
|
|
24
|
+
if (!fs.existsSync(indexPath)) {
|
|
25
|
+
throw new Error(`index.html not found in ${distDir}\n` +
|
|
26
|
+
` The build output must contain an index.html at the root.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function validateAndCollectAssets(packageDir) {
|
|
30
|
+
if (!fs.existsSync(packageDir)) {
|
|
31
|
+
throw new Error(`.appstrata/package/ directory not found.\n` +
|
|
32
|
+
` Create it with at least a logo file and one screenshot:\n` +
|
|
33
|
+
` .appstrata/package/logo.png\n` +
|
|
34
|
+
` .appstrata/package/screenshots/1.png`);
|
|
35
|
+
}
|
|
36
|
+
const logoFiles = fs.readdirSync(packageDir).filter((f) => {
|
|
37
|
+
const name = path.parse(f).name.toLowerCase();
|
|
38
|
+
return name === "logo" && fs.statSync(path.join(packageDir, f)).isFile();
|
|
39
|
+
});
|
|
40
|
+
if (logoFiles.length === 0) {
|
|
41
|
+
throw new Error(`No logo file found in .appstrata/package/.\n` +
|
|
42
|
+
` Add a logo file (e.g., logo.png, logo.svg).`);
|
|
43
|
+
}
|
|
44
|
+
if (logoFiles.length > 1) {
|
|
45
|
+
throw new Error(`Multiple logo files found in .appstrata/package/: ${logoFiles.join(", ")}\n` +
|
|
46
|
+
` Keep exactly one logo file.`);
|
|
47
|
+
}
|
|
48
|
+
const logoFile = logoFiles[0];
|
|
49
|
+
const logoExt = path.extname(logoFile);
|
|
50
|
+
const screenshotsDir = path.join(packageDir, "screenshots");
|
|
51
|
+
if (!fs.existsSync(screenshotsDir) || !fs.statSync(screenshotsDir).isDirectory()) {
|
|
52
|
+
throw new Error(`screenshots/ directory not found in .appstrata/package/.\n` +
|
|
53
|
+
` Create it with at least one screenshot:\n` +
|
|
54
|
+
` .appstrata/package/screenshots/1.png`);
|
|
55
|
+
}
|
|
56
|
+
const screenshotFiles = fs.readdirSync(screenshotsDir)
|
|
57
|
+
.filter((f) => fs.statSync(path.join(screenshotsDir, f)).isFile())
|
|
58
|
+
.map((f) => ({
|
|
59
|
+
name: path.parse(f).name,
|
|
60
|
+
ext: path.extname(f),
|
|
61
|
+
fullPath: path.join(screenshotsDir, f),
|
|
62
|
+
}));
|
|
63
|
+
if (screenshotFiles.length === 0) {
|
|
64
|
+
throw new Error(`No screenshots found in .appstrata/package/screenshots/.\n` +
|
|
65
|
+
` Add at least one screenshot image.`);
|
|
66
|
+
}
|
|
67
|
+
const expectedEntries = new Set(["logo" + logoExt.toLowerCase(), "screenshots", "schema.json"]);
|
|
68
|
+
const entries = fs.readdirSync(packageDir);
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (!expectedEntries.has(entry.toLowerCase())) {
|
|
71
|
+
console.warn(` Warning: unexpected file in .appstrata/package/: ${entry} (skipping)`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { logoFile, logoExt, screenshotFiles };
|
|
75
|
+
}
|
|
76
|
+
function handleSchema(packageDir, distDir, config) {
|
|
77
|
+
const manualSchemaPath = path.join(packageDir, "schema.json");
|
|
78
|
+
if (fs.existsSync(manualSchemaPath)) {
|
|
79
|
+
fs.copyFileSync(manualSchemaPath, path.join(distDir, "schema.json"));
|
|
80
|
+
return "manual";
|
|
81
|
+
}
|
|
82
|
+
const schema = generateYodeckSchema(config.app);
|
|
83
|
+
fs.writeFileSync(path.join(distDir, "schema.json"), JSON.stringify(schema, null, "\t"), "utf-8");
|
|
84
|
+
return "generated";
|
|
85
|
+
}
|
|
86
|
+
function copyAssets(packageDir, distDir, appId, assets) {
|
|
87
|
+
const fileassetsDir = path.join(distDir, "_fileassets");
|
|
88
|
+
fs.mkdirSync(fileassetsDir, { recursive: true });
|
|
89
|
+
const logoSrc = path.join(packageDir, assets.logoFile);
|
|
90
|
+
const logoDest = path.join(fileassetsDir, `${appId}{logo}${assets.logoExt}`);
|
|
91
|
+
fs.copyFileSync(logoSrc, logoDest);
|
|
92
|
+
for (const screenshot of assets.screenshotFiles) {
|
|
93
|
+
const dest = path.join(fileassetsDir, `${screenshot.name}{slides}${screenshot.ext}`);
|
|
94
|
+
fs.copyFileSync(screenshot.fullPath, dest);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function createZip(distDir, zipPath) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const output = fs.createWriteStream(zipPath);
|
|
100
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
101
|
+
output.on("close", () => resolve(archive.pointer()));
|
|
102
|
+
archive.on("error", (err) => reject(err));
|
|
103
|
+
archive.pipe(output);
|
|
104
|
+
archive.glob("**/*", {
|
|
105
|
+
cwd: distDir,
|
|
106
|
+
ignore: ["*.zip"],
|
|
107
|
+
});
|
|
108
|
+
archive.finalize();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function formatSize(bytes) {
|
|
112
|
+
if (bytes < 1024)
|
|
113
|
+
return `${bytes} B`;
|
|
114
|
+
if (bytes < 1024 * 1024)
|
|
115
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
116
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
117
|
+
}
|
|
118
|
+
export async function packageCommand(options) {
|
|
119
|
+
try {
|
|
120
|
+
const result = await loadConfig(options.config);
|
|
121
|
+
if (!result) {
|
|
122
|
+
throw new Error(`Failed to load config file: ${options.config}`);
|
|
123
|
+
}
|
|
124
|
+
const config = result.config;
|
|
125
|
+
const appId = config.app.id;
|
|
126
|
+
const appName = config.app.name;
|
|
127
|
+
const appVersion = config.app.version;
|
|
128
|
+
console.log(`\n Packaging ${appName} v${appVersion}...\n`);
|
|
129
|
+
const distDir = path.resolve(process.cwd(), options.outDir);
|
|
130
|
+
const isCloudApp = !!config.app.url;
|
|
131
|
+
if (isCloudApp) {
|
|
132
|
+
if (!fs.existsSync(distDir)) {
|
|
133
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
console.log(` App URL: ${config.app.url}`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
validateBuildOutput(distDir);
|
|
139
|
+
console.log(` Build output: ${options.outDir}/`);
|
|
140
|
+
}
|
|
141
|
+
const packageDir = path.resolve(process.cwd(), PACKAGE_DIR);
|
|
142
|
+
const assets = validateAndCollectAssets(packageDir);
|
|
143
|
+
const schemaMode = handleSchema(packageDir, distDir, config);
|
|
144
|
+
console.log(` Schema: ${schemaMode === "manual" ? "copied from .appstrata/package/schema.json" : "generated from app.configuration.inputs"}`);
|
|
145
|
+
copyAssets(packageDir, distDir, appId, assets);
|
|
146
|
+
console.log(` Logo: .appstrata/package/${assets.logoFile} -> _fileassets/${appId}{logo}${assets.logoExt}`);
|
|
147
|
+
console.log(` Screenshots: ${assets.screenshotFiles.length} file(s) -> _fileassets/*{slides}.*`);
|
|
148
|
+
const zipFileName = `${appId}.zip`;
|
|
149
|
+
const zipPath = path.join(distDir, zipFileName);
|
|
150
|
+
if (fs.existsSync(zipPath)) {
|
|
151
|
+
fs.unlinkSync(zipPath);
|
|
152
|
+
}
|
|
153
|
+
const size = await createZip(distDir, zipPath);
|
|
154
|
+
console.log(`\n Created: ${options.outDir}/${zipFileName} (${formatSize(size)})\n`);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
158
|
+
console.error(`\n Error: ${message}\n`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;GAIG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @appstrata/cli - CLI for AppStrata Development Tools
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { devCommand } from "./commands/dev.js";
|
|
9
|
+
import { devHttpPlayerCommand } from "./commands/dev-http-player.js";
|
|
10
|
+
import { packageCommand } from "./commands/package.js";
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("appstrata")
|
|
14
|
+
.description("AppStrata Development Tools CLI")
|
|
15
|
+
.version("1.0.0");
|
|
16
|
+
program
|
|
17
|
+
.command("dev")
|
|
18
|
+
.description("Start development player with iframe")
|
|
19
|
+
.requiredOption("-u, --url <url>", "URL of app to load in player iframe")
|
|
20
|
+
.option("-p, --port <port>", "Port to run dev server on", "5173")
|
|
21
|
+
.option("-c, --config <path>", "Path to appstrata.config.ts", "appstrata.config.ts")
|
|
22
|
+
.option("--host", "Expose server to network")
|
|
23
|
+
.option("-r, --relay <url>", "URL of remote HTTP player to relay to")
|
|
24
|
+
.option("-t, --transport <mode>", "HTTP transport mode for relay (sse or polling)", "sse")
|
|
25
|
+
.action(devCommand);
|
|
26
|
+
program
|
|
27
|
+
.command("dev-http-player")
|
|
28
|
+
.description("Start dev player with HTTP transport only")
|
|
29
|
+
.option("-p, --port <port>", "Port for HTTP server", "5175")
|
|
30
|
+
.option("--host", "Expose server to network")
|
|
31
|
+
.option("-c, --config <path>", "Path to appstrata.config.ts", "appstrata.config.ts")
|
|
32
|
+
.option("-t, --transport <mode>", "HTTP transport mode (sse or polling)", "sse")
|
|
33
|
+
.option("--runtime <runtime>", "Player runtime (node, python)", "node")
|
|
34
|
+
.action(devHttpPlayerCommand);
|
|
35
|
+
program
|
|
36
|
+
.command("package")
|
|
37
|
+
.description("Package app into a zip bundle for CMS deployment")
|
|
38
|
+
.option("-c, --config <path>", "Path to appstrata.config.ts", "appstrata.config.ts")
|
|
39
|
+
.option("-o, --outDir <dir>", "Build output directory", "dist")
|
|
40
|
+
.action(packageCommand);
|
|
41
|
+
program.parse();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External runtime registry.
|
|
3
|
+
*
|
|
4
|
+
* Maps runtime names to their implementations.
|
|
5
|
+
* To add a new runtime, import it here and register it in the map.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExternalRuntime } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Get an external runtime by name.
|
|
10
|
+
*
|
|
11
|
+
* @throws If the runtime is not registered.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getRuntime(name: string): ExternalRuntime;
|
|
14
|
+
/**
|
|
15
|
+
* List all registered external runtime names.
|
|
16
|
+
*/
|
|
17
|
+
export declare function listRuntimes(): string[];
|
|
18
|
+
export type { ExternalRuntime, RuntimeProcess, RuntimeSpawnOptions } from "./types.js";
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtimes/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAOlD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CASxD;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,EAAE,CAEvC;AAED,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External runtime registry.
|
|
3
|
+
*
|
|
4
|
+
* Maps runtime names to their implementations.
|
|
5
|
+
* To add a new runtime, import it here and register it in the map.
|
|
6
|
+
*/
|
|
7
|
+
import { pythonRuntime } from "./python.js";
|
|
8
|
+
const runtimes = {
|
|
9
|
+
python: pythonRuntime,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Get an external runtime by name.
|
|
13
|
+
*
|
|
14
|
+
* @throws If the runtime is not registered.
|
|
15
|
+
*/
|
|
16
|
+
export function getRuntime(name) {
|
|
17
|
+
const runtime = runtimes[name];
|
|
18
|
+
if (!runtime) {
|
|
19
|
+
const available = Object.keys(runtimes).join(", ");
|
|
20
|
+
throw new Error(`Unknown runtime "${name}". Available external runtimes: ${available}`);
|
|
21
|
+
}
|
|
22
|
+
return runtime;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* List all registered external runtime names.
|
|
26
|
+
*/
|
|
27
|
+
export function listRuntimes() {
|
|
28
|
+
return Object.keys(runtimes);
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python runtime implementation.
|
|
3
|
+
*
|
|
4
|
+
* Manages a Python virtual environment and spawns the Python player process.
|
|
5
|
+
*
|
|
6
|
+
* - Venv is created at `{projectRoot}/.appstrata/python-venv/`
|
|
7
|
+
* - Dependencies are installed from `player-lib-python/requirements.txt`
|
|
8
|
+
* - The Python player library is at `packages/player-lib-python/appstrata_player/`
|
|
9
|
+
* - The dev-player entry point is at `packages/cli/python/appstrata_dev_player/`
|
|
10
|
+
*/
|
|
11
|
+
import type { ExternalRuntime } from "./types.js";
|
|
12
|
+
export declare const pythonRuntime: ExternalRuntime;
|
|
13
|
+
//# sourceMappingURL=python.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"python.d.ts","sourceRoot":"","sources":["../../src/runtimes/python.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAuC,MAAM,YAAY,CAAC;AAsCvF,eAAO,MAAM,aAAa,EAAE,eA6F3B,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python runtime implementation.
|
|
3
|
+
*
|
|
4
|
+
* Manages a Python virtual environment and spawns the Python player process.
|
|
5
|
+
*
|
|
6
|
+
* - Venv is created at `{projectRoot}/.appstrata/python-venv/`
|
|
7
|
+
* - Dependencies are installed from `player-lib-python/requirements.txt`
|
|
8
|
+
* - The Python player library is at `packages/player-lib-python/appstrata_player/`
|
|
9
|
+
* - The dev-player entry point is at `packages/cli/python/appstrata_dev_player/`
|
|
10
|
+
*/
|
|
11
|
+
import { execSync, spawn } from "node:child_process";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
/** Path to the Python player-lib package directory (library). */
|
|
18
|
+
const PYTHON_LIB_DIR = path.resolve(__dirname, "../../../player-lib-python");
|
|
19
|
+
/** Path to the Python dev-player module (CLI entry point). */
|
|
20
|
+
const DEV_PLAYER_DIR = path.resolve(__dirname, "../../python");
|
|
21
|
+
/**
|
|
22
|
+
* Find a working system Python (>= 3.10).
|
|
23
|
+
* Tries python3, then python.
|
|
24
|
+
*/
|
|
25
|
+
function findSystemPython() {
|
|
26
|
+
for (const candidate of ["python3", "python"]) {
|
|
27
|
+
try {
|
|
28
|
+
const version = execSync(`${candidate} --version 2>&1`, { encoding: "utf-8" }).trim();
|
|
29
|
+
// Ensure it's at least 3.10
|
|
30
|
+
const match = version.match(/Python (\d+)\.(\d+)/);
|
|
31
|
+
if (match) {
|
|
32
|
+
const major = parseInt(match[1], 10);
|
|
33
|
+
const minor = parseInt(match[2], 10);
|
|
34
|
+
if (major === 3 && minor >= 10) {
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Not found, try next
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error("Python 3.10+ is required but not found on PATH. " +
|
|
44
|
+
"Install Python and ensure 'python3' or 'python' is available.");
|
|
45
|
+
}
|
|
46
|
+
export const pythonRuntime = {
|
|
47
|
+
name: "python",
|
|
48
|
+
async ensureReady(projectRoot) {
|
|
49
|
+
const venvDir = path.join(projectRoot, ".appstrata", "python-venv");
|
|
50
|
+
const venvPython = path.join(venvDir, "bin", "python");
|
|
51
|
+
if (fs.existsSync(venvPython)) {
|
|
52
|
+
// Venv already exists — check if deps are installed
|
|
53
|
+
try {
|
|
54
|
+
execSync(`${venvPython} -c "import aiohttp"`, { stdio: "ignore" });
|
|
55
|
+
console.log(" Python venv ready at", venvDir);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// aiohttp not installed, reinstall below
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log(" Setting up Python environment...");
|
|
63
|
+
const systemPython = findSystemPython();
|
|
64
|
+
console.log(` Found ${systemPython}`);
|
|
65
|
+
// Create venv
|
|
66
|
+
if (!fs.existsSync(venvDir)) {
|
|
67
|
+
fs.mkdirSync(venvDir, { recursive: true });
|
|
68
|
+
console.log(" Creating venv...");
|
|
69
|
+
execSync(`${systemPython} -m venv "${venvDir}"`, { stdio: "inherit" });
|
|
70
|
+
}
|
|
71
|
+
// Install dependencies
|
|
72
|
+
const requirementsPath = path.join(PYTHON_LIB_DIR, "requirements.txt");
|
|
73
|
+
console.log(" Installing Python dependencies...");
|
|
74
|
+
execSync(`"${venvPython}" -m pip install -q -r "${requirementsPath}"`, {
|
|
75
|
+
stdio: "inherit",
|
|
76
|
+
});
|
|
77
|
+
console.log(" Python environment ready");
|
|
78
|
+
},
|
|
79
|
+
spawn(options) {
|
|
80
|
+
const venvPython = path.join(process.cwd(), ".appstrata", "python-venv", "bin", "python");
|
|
81
|
+
const args = [
|
|
82
|
+
"-m",
|
|
83
|
+
"appstrata_dev_player",
|
|
84
|
+
"--port",
|
|
85
|
+
String(options.port),
|
|
86
|
+
"--transport",
|
|
87
|
+
options.transport,
|
|
88
|
+
];
|
|
89
|
+
if (options.host) {
|
|
90
|
+
args.push("--host");
|
|
91
|
+
}
|
|
92
|
+
const child = spawn(venvPython, args, {
|
|
93
|
+
env: {
|
|
94
|
+
...process.env,
|
|
95
|
+
PYTHONPATH: [PYTHON_LIB_DIR, DEV_PLAYER_DIR].join(path.delimiter),
|
|
96
|
+
PYTHONUNBUFFERED: "1",
|
|
97
|
+
},
|
|
98
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
99
|
+
});
|
|
100
|
+
// Promise that resolves when the process exits
|
|
101
|
+
const exited = new Promise((resolve) => {
|
|
102
|
+
child.on("exit", (code) => resolve(code));
|
|
103
|
+
child.on("error", () => resolve(null));
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
child,
|
|
107
|
+
sendConfig(config) {
|
|
108
|
+
if (child.stdin && !child.stdin.destroyed) {
|
|
109
|
+
child.stdin.write(JSON.stringify(config) + "\n");
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
kill() {
|
|
113
|
+
if (!child.killed) {
|
|
114
|
+
child.kill("SIGTERM");
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
exited,
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External runtime abstraction.
|
|
3
|
+
*
|
|
4
|
+
* Defines the interface that all external player runtimes (Python, Go, etc.)
|
|
5
|
+
* must implement. This keeps `dev-http-player.ts` free of runtime-specific
|
|
6
|
+
* logic — adding a new runtime is just a new file in this directory.
|
|
7
|
+
*/
|
|
8
|
+
import type { ChildProcess } from "node:child_process";
|
|
9
|
+
/**
|
|
10
|
+
* Options passed to the runtime when spawning.
|
|
11
|
+
*/
|
|
12
|
+
export interface RuntimeSpawnOptions {
|
|
13
|
+
port: number;
|
|
14
|
+
host: boolean;
|
|
15
|
+
transport: "sse" | "polling";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Handle to a running external runtime process.
|
|
19
|
+
*/
|
|
20
|
+
export interface RuntimeProcess {
|
|
21
|
+
/** The underlying child process. */
|
|
22
|
+
child: ChildProcess;
|
|
23
|
+
/**
|
|
24
|
+
* Send a config object to the runtime process.
|
|
25
|
+
* Serialized as a single JSON line written to stdin.
|
|
26
|
+
*/
|
|
27
|
+
sendConfig(config: object): void;
|
|
28
|
+
/**
|
|
29
|
+
* Kill the runtime process.
|
|
30
|
+
*/
|
|
31
|
+
kill(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Promise that resolves with the exit code when the process exits.
|
|
34
|
+
*/
|
|
35
|
+
exited: Promise<number | null>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* External runtime interface.
|
|
39
|
+
*
|
|
40
|
+
* Each runtime (Python, Go, etc.) implements this interface.
|
|
41
|
+
* The `dev-http-player` command uses it to delegate execution.
|
|
42
|
+
*
|
|
43
|
+
* @example Adding a new runtime:
|
|
44
|
+
* ```ts
|
|
45
|
+
* // runtimes/go.ts
|
|
46
|
+
* export const goRuntime: ExternalRuntime = {
|
|
47
|
+
* name: "go",
|
|
48
|
+
* async ensureReady(projectRoot) { ... },
|
|
49
|
+
* spawn(options) { ... },
|
|
50
|
+
* };
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export interface ExternalRuntime {
|
|
54
|
+
/** Display name of the runtime (e.g., "python", "go"). */
|
|
55
|
+
readonly name: string;
|
|
56
|
+
/**
|
|
57
|
+
* Ensure the runtime environment is ready.
|
|
58
|
+
*
|
|
59
|
+
* For Python: creates a venv and installs aiohttp.
|
|
60
|
+
* For Go: might compile the binary, etc.
|
|
61
|
+
*
|
|
62
|
+
* @param projectRoot - Absolute path to the project root.
|
|
63
|
+
* @throws If the runtime cannot be set up (e.g., Python not installed).
|
|
64
|
+
*/
|
|
65
|
+
ensureReady(projectRoot: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Spawn the runtime process.
|
|
68
|
+
*
|
|
69
|
+
* @param options - Port, host, transport mode.
|
|
70
|
+
* @returns Handle to the running process.
|
|
71
|
+
*/
|
|
72
|
+
spawn(options: RuntimeSpawnOptions): RuntimeProcess;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runtimes/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,KAAK,GAAG,SAAS,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,KAAK,EAAE,YAAY,CAAC;IAEpB;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAEjC;;OAEG;IACH,IAAI,IAAI,IAAI,CAAC;IAEb;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;;;;;OAQG;IACH,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,mBAAmB,GAAG,cAAc,CAAC;CACrD"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External runtime abstraction.
|
|
3
|
+
*
|
|
4
|
+
* Defines the interface that all external player runtimes (Python, Go, etc.)
|
|
5
|
+
* must implement. This keeps `dev-http-player.ts` free of runtime-specific
|
|
6
|
+
* logic — adding a new runtime is just a new file in this directory.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|