@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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a Yodeck-compatible schema.json from AppStrata configuration.
|
|
3
|
+
*
|
|
4
|
+
* Transforms `app.configuration.inputs` (platform-agnostic field definitions)
|
|
5
|
+
* into Yodeck's schema.json format with appropriate editor types and attributes.
|
|
6
|
+
*/
|
|
7
|
+
import type { AppConfig } from "@appstrata/dev";
|
|
8
|
+
export interface YodeckSchema {
|
|
9
|
+
meta: {
|
|
10
|
+
application_guid: string;
|
|
11
|
+
application_version: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
details?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
fields: string[];
|
|
18
|
+
styleSettings: unknown[];
|
|
19
|
+
data: Record<string, unknown>;
|
|
20
|
+
schema: Record<string, YodeckFieldDefinition>;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface YodeckFieldDefinition {
|
|
24
|
+
type: string;
|
|
25
|
+
title: string;
|
|
26
|
+
help?: string;
|
|
27
|
+
validators?: string[];
|
|
28
|
+
def?: unknown;
|
|
29
|
+
options?: {
|
|
30
|
+
val: string;
|
|
31
|
+
label: string;
|
|
32
|
+
}[];
|
|
33
|
+
editorAttrs?: Record<string, unknown>;
|
|
34
|
+
editorClass?: string;
|
|
35
|
+
select2options?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate a Yodeck-compatible schema.json from AppStrata app configuration.
|
|
39
|
+
*/
|
|
40
|
+
export declare function generateYodeckSchema(appConfig: AppConfig): YodeckSchema;
|
|
41
|
+
//# sourceMappingURL=schema-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-generator.d.ts","sourceRoot":"","sources":["../src/schema-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,SAAS,EAUV,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE;QACJ,gBAAgB,EAAE,MAAM,CAAC;QACzB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,aAAa,EAAE,OAAO,EAAE,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IAC9C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAuMD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,GAAG,YAAY,CA0CvE"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a Yodeck-compatible schema.json from AppStrata configuration.
|
|
3
|
+
*
|
|
4
|
+
* Transforms `app.configuration.inputs` (platform-agnostic field definitions)
|
|
5
|
+
* into Yodeck's schema.json format with appropriate editor types and attributes.
|
|
6
|
+
*/
|
|
7
|
+
function transformTextInput(input) {
|
|
8
|
+
const field = {
|
|
9
|
+
type: "Text",
|
|
10
|
+
title: input.title,
|
|
11
|
+
};
|
|
12
|
+
if (input.help)
|
|
13
|
+
field.help = input.help;
|
|
14
|
+
if (input.default !== undefined)
|
|
15
|
+
field.def = input.default;
|
|
16
|
+
const validators = [];
|
|
17
|
+
if (input.validators?.required)
|
|
18
|
+
validators.push("required");
|
|
19
|
+
if (validators.length > 0)
|
|
20
|
+
field.validators = validators;
|
|
21
|
+
if (input.validators?.minLength !== undefined || input.validators?.maxLength !== undefined) {
|
|
22
|
+
field.editorAttrs = {};
|
|
23
|
+
if (input.validators?.minLength !== undefined)
|
|
24
|
+
field.editorAttrs.minlength = input.validators.minLength;
|
|
25
|
+
if (input.validators?.maxLength !== undefined)
|
|
26
|
+
field.editorAttrs.maxlength = input.validators.maxLength;
|
|
27
|
+
}
|
|
28
|
+
return field;
|
|
29
|
+
}
|
|
30
|
+
function transformColorInput(input) {
|
|
31
|
+
const field = {
|
|
32
|
+
type: "SpectrumColorPicker",
|
|
33
|
+
title: input.title,
|
|
34
|
+
};
|
|
35
|
+
if (input.help)
|
|
36
|
+
field.help = input.help;
|
|
37
|
+
if (input.default !== undefined) {
|
|
38
|
+
const hex = input.default.replace(/^#/, "");
|
|
39
|
+
field.editorAttrs = { def: hex };
|
|
40
|
+
}
|
|
41
|
+
const validators = [];
|
|
42
|
+
if (input.validators?.required)
|
|
43
|
+
validators.push("required");
|
|
44
|
+
if (validators.length > 0)
|
|
45
|
+
field.validators = validators;
|
|
46
|
+
return field;
|
|
47
|
+
}
|
|
48
|
+
function transformBooleanInput(input) {
|
|
49
|
+
const field = {
|
|
50
|
+
type: "Boolean",
|
|
51
|
+
title: input.title,
|
|
52
|
+
};
|
|
53
|
+
if (input.help)
|
|
54
|
+
field.help = input.help;
|
|
55
|
+
if (input.default !== undefined) {
|
|
56
|
+
field.editorAttrs = { def: input.default };
|
|
57
|
+
}
|
|
58
|
+
return field;
|
|
59
|
+
}
|
|
60
|
+
function transformNumberInput(input) {
|
|
61
|
+
const field = {
|
|
62
|
+
type: "Spinner",
|
|
63
|
+
title: input.title,
|
|
64
|
+
};
|
|
65
|
+
if (input.help)
|
|
66
|
+
field.help = input.help;
|
|
67
|
+
const attrs = {};
|
|
68
|
+
if (input.default !== undefined)
|
|
69
|
+
attrs.def = input.default;
|
|
70
|
+
if (input.min !== undefined)
|
|
71
|
+
attrs.min = input.min;
|
|
72
|
+
if (input.max !== undefined)
|
|
73
|
+
attrs.max = input.max;
|
|
74
|
+
if (input.step !== undefined)
|
|
75
|
+
attrs.step = input.step;
|
|
76
|
+
if (Object.keys(attrs).length > 0)
|
|
77
|
+
field.editorAttrs = attrs;
|
|
78
|
+
const validators = [];
|
|
79
|
+
if (input.validators?.required)
|
|
80
|
+
validators.push("required");
|
|
81
|
+
if (validators.length > 0)
|
|
82
|
+
field.validators = validators;
|
|
83
|
+
return field;
|
|
84
|
+
}
|
|
85
|
+
function transformSelectInput(input) {
|
|
86
|
+
const field = {
|
|
87
|
+
type: "Select2",
|
|
88
|
+
title: input.title,
|
|
89
|
+
editorClass: "select2container",
|
|
90
|
+
select2options: {
|
|
91
|
+
closeOnSelect: true,
|
|
92
|
+
minimumResultsForSearch: -1,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
if (input.help)
|
|
96
|
+
field.help = input.help;
|
|
97
|
+
if (input.default !== undefined)
|
|
98
|
+
field.def = input.default;
|
|
99
|
+
field.options = input.options.map((opt) => ({
|
|
100
|
+
val: opt.value,
|
|
101
|
+
label: opt.label,
|
|
102
|
+
}));
|
|
103
|
+
const validators = [];
|
|
104
|
+
if (input.validators?.required)
|
|
105
|
+
validators.push("required");
|
|
106
|
+
if (validators.length > 0)
|
|
107
|
+
field.validators = validators;
|
|
108
|
+
return field;
|
|
109
|
+
}
|
|
110
|
+
function transformRangeInput(input) {
|
|
111
|
+
const field = {
|
|
112
|
+
type: "Slider",
|
|
113
|
+
title: input.title,
|
|
114
|
+
};
|
|
115
|
+
if (input.help)
|
|
116
|
+
field.help = input.help;
|
|
117
|
+
const attrs = {};
|
|
118
|
+
if (input.default !== undefined)
|
|
119
|
+
attrs.def = input.default;
|
|
120
|
+
attrs.min = input.min;
|
|
121
|
+
attrs.max = input.max;
|
|
122
|
+
if (input.step !== undefined)
|
|
123
|
+
attrs.step = input.step;
|
|
124
|
+
field.editorAttrs = attrs;
|
|
125
|
+
const validators = [];
|
|
126
|
+
if (input.validators?.required)
|
|
127
|
+
validators.push("required");
|
|
128
|
+
if (validators.length > 0)
|
|
129
|
+
field.validators = validators;
|
|
130
|
+
return field;
|
|
131
|
+
}
|
|
132
|
+
function transformFontInput(input) {
|
|
133
|
+
const field = {
|
|
134
|
+
type: "FontFamily",
|
|
135
|
+
title: input.title,
|
|
136
|
+
editorClass: "select2container full-size",
|
|
137
|
+
};
|
|
138
|
+
if (input.help)
|
|
139
|
+
field.help = input.help;
|
|
140
|
+
const validators = [];
|
|
141
|
+
if (input.validators?.required)
|
|
142
|
+
validators.push("required");
|
|
143
|
+
if (validators.length > 0)
|
|
144
|
+
field.validators = validators;
|
|
145
|
+
return field;
|
|
146
|
+
}
|
|
147
|
+
function transformImageInput(input) {
|
|
148
|
+
const field = {
|
|
149
|
+
type: "mediaFilePicker",
|
|
150
|
+
title: input.title,
|
|
151
|
+
};
|
|
152
|
+
if (input.help)
|
|
153
|
+
field.help = input.help;
|
|
154
|
+
const validators = [];
|
|
155
|
+
if (input.validators?.required)
|
|
156
|
+
validators.push("required");
|
|
157
|
+
if (validators.length > 0)
|
|
158
|
+
field.validators = validators;
|
|
159
|
+
return field;
|
|
160
|
+
}
|
|
161
|
+
function transformInput(input) {
|
|
162
|
+
switch (input.type) {
|
|
163
|
+
case "text": return transformTextInput(input);
|
|
164
|
+
case "color": return transformColorInput(input);
|
|
165
|
+
case "boolean": return transformBooleanInput(input);
|
|
166
|
+
case "number": return transformNumberInput(input);
|
|
167
|
+
case "select": return transformSelectInput(input);
|
|
168
|
+
case "range": return transformRangeInput(input);
|
|
169
|
+
case "font": return transformFontInput(input);
|
|
170
|
+
case "image": return transformImageInput(input);
|
|
171
|
+
default: {
|
|
172
|
+
const _exhaustive = input;
|
|
173
|
+
throw new Error(`Unknown input type: ${_exhaustive.type}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function getDefaultValue(input) {
|
|
178
|
+
if (input.type === "font" || input.type === "image")
|
|
179
|
+
return undefined;
|
|
180
|
+
return input.default;
|
|
181
|
+
}
|
|
182
|
+
function isPlainObject(value) {
|
|
183
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Recursively merge `override` into `base`. Plain objects are merged
|
|
187
|
+
* key-by-key; everything else (arrays, primitives) is replaced outright.
|
|
188
|
+
* `override` values take precedence.
|
|
189
|
+
*/
|
|
190
|
+
function deepMerge(base, override) {
|
|
191
|
+
const result = { ...base };
|
|
192
|
+
for (const key of Object.keys(override)) {
|
|
193
|
+
if (isPlainObject(result[key]) && isPlainObject(override[key])) {
|
|
194
|
+
result[key] = deepMerge(result[key], override[key]);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
result[key] = override[key];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Generate a Yodeck-compatible schema.json from AppStrata app configuration.
|
|
204
|
+
*/
|
|
205
|
+
export function generateYodeckSchema(appConfig) {
|
|
206
|
+
const inputs = appConfig.configuration?.inputs ?? {};
|
|
207
|
+
const keys = Object.keys(inputs);
|
|
208
|
+
const data = {};
|
|
209
|
+
const schema = {};
|
|
210
|
+
const fieldKeys = [];
|
|
211
|
+
for (const key of keys) {
|
|
212
|
+
const input = inputs[key];
|
|
213
|
+
const schemaKey = input.type === "image" ? `${key}__file` : key;
|
|
214
|
+
fieldKeys.push(schemaKey);
|
|
215
|
+
schema[schemaKey] = transformInput(input);
|
|
216
|
+
const defaultVal = getDefaultValue(input);
|
|
217
|
+
if (defaultVal !== undefined) {
|
|
218
|
+
data[schemaKey] = defaultVal;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const generated = {
|
|
222
|
+
meta: {
|
|
223
|
+
application_guid: appConfig.id,
|
|
224
|
+
application_version: appConfig.version,
|
|
225
|
+
name: appConfig.name,
|
|
226
|
+
description: appConfig.description,
|
|
227
|
+
details: appConfig.details,
|
|
228
|
+
},
|
|
229
|
+
...(appConfig.url ? { url: appConfig.url } : {}),
|
|
230
|
+
fields: fieldKeys,
|
|
231
|
+
styleSettings: [],
|
|
232
|
+
data,
|
|
233
|
+
schema,
|
|
234
|
+
};
|
|
235
|
+
if (appConfig.properties) {
|
|
236
|
+
return deepMerge(generated, appConfig.properties);
|
|
237
|
+
}
|
|
238
|
+
return generated;
|
|
239
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-page.d.ts","sourceRoot":"","sources":["../src/status-page.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAoEzE"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML status page for dev player commands.
|
|
3
|
+
*/
|
|
4
|
+
export function renderStatusPage(port, sessions) {
|
|
5
|
+
const sessionList = sessions.length === 0
|
|
6
|
+
? '<p>No active sessions</p>'
|
|
7
|
+
: sessions.map(s => `<div class="session">${s}</div>`).join('');
|
|
8
|
+
return `<!DOCTYPE html>
|
|
9
|
+
<html>
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<title>AppStrata HTTP Dev Player</title>
|
|
13
|
+
<style>
|
|
14
|
+
body {
|
|
15
|
+
font-family: system-ui, sans-serif;
|
|
16
|
+
max-width: 800px;
|
|
17
|
+
margin: 50px auto;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
background: #f5f5f5;
|
|
20
|
+
}
|
|
21
|
+
h1 {
|
|
22
|
+
color: #333;
|
|
23
|
+
border-bottom: 2px solid #667eea;
|
|
24
|
+
padding-bottom: 10px;
|
|
25
|
+
}
|
|
26
|
+
.info {
|
|
27
|
+
background: white;
|
|
28
|
+
padding: 20px;
|
|
29
|
+
border-radius: 8px;
|
|
30
|
+
margin: 20px 0;
|
|
31
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
32
|
+
}
|
|
33
|
+
.endpoint {
|
|
34
|
+
background: #f8f9fa;
|
|
35
|
+
padding: 10px;
|
|
36
|
+
border-left: 3px solid #667eea;
|
|
37
|
+
margin: 10px 0;
|
|
38
|
+
font-family: monospace;
|
|
39
|
+
}
|
|
40
|
+
code {
|
|
41
|
+
background: #f0f0f0;
|
|
42
|
+
padding: 2px 6px;
|
|
43
|
+
border-radius: 3px;
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
}
|
|
46
|
+
.sessions { margin-top: 20px; }
|
|
47
|
+
.session {
|
|
48
|
+
background: #e8f5e9;
|
|
49
|
+
padding: 10px;
|
|
50
|
+
margin: 5px 0;
|
|
51
|
+
border-radius: 4px;
|
|
52
|
+
font-family: monospace;
|
|
53
|
+
font-size: 14px;
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<h1>AppStrata HTTP Dev Player</h1>
|
|
59
|
+
<div class="info">
|
|
60
|
+
<h3>Status: Running</h3>
|
|
61
|
+
<p>This server implements the AppStrata protocol over HTTP.</p>
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
<div class="sessions">
|
|
65
|
+
<h3>Active Sessions:</h3>
|
|
66
|
+
<div id="sessions">${sessionList}</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</body>
|
|
70
|
+
</html>`;
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appstrata/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AppStrata CLI - Command-line tools for digital signage app development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"appstrata": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"python/**/*.py"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"digital-signage",
|
|
23
|
+
"appstrata",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"author": "AppStrata",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "restricted"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"archiver": "^7.0.1",
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"vite": "^6.0.0",
|
|
35
|
+
"@appstrata/core": "0.1.0",
|
|
36
|
+
"@appstrata/player-lib": "0.1.0",
|
|
37
|
+
"@appstrata/dev": "0.1.0",
|
|
38
|
+
"@appstrata/protocol": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/archiver": "^7.0.0",
|
|
42
|
+
"@types/node": "^24.10.1",
|
|
43
|
+
"typescript": "^5.3.3"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc --build tsconfig.build.json && chmod +x dist/index.js",
|
|
47
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
48
|
+
"dev": "tsc --build --watch"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AppStrata dev HTTP player — CLI entry point for the Python runtime."""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry point for the AppStrata Python player.
|
|
3
|
+
|
|
4
|
+
Usage (invoked by Node CLI):
|
|
5
|
+
python -m appstrata_player --port 5175 --transport sse
|
|
6
|
+
|
|
7
|
+
Config is received via stdin as newline-delimited JSON.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
import signal
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from .config import StdinConfigReader
|
|
19
|
+
from .server import DevHttpPlayerServer
|
|
20
|
+
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level=logging.INFO,
|
|
23
|
+
format="%(message)s",
|
|
24
|
+
stream=sys.stderr,
|
|
25
|
+
)
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_args() -> argparse.Namespace:
|
|
30
|
+
parser = argparse.ArgumentParser(
|
|
31
|
+
description="AppStrata HTTP Dev Player (Python runtime)"
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--port",
|
|
35
|
+
type=int,
|
|
36
|
+
default=5175,
|
|
37
|
+
help="Port for HTTP server (default: 5175)",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--host",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Expose server to network (bind 0.0.0.0)",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--transport",
|
|
46
|
+
choices=["sse", "polling"],
|
|
47
|
+
default="sse",
|
|
48
|
+
help="HTTP transport mode (default: sse)",
|
|
49
|
+
)
|
|
50
|
+
return parser.parse_args()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def main() -> None:
|
|
54
|
+
args = parse_args()
|
|
55
|
+
|
|
56
|
+
# Read initial config from stdin (blocks until first JSON line arrives)
|
|
57
|
+
config_reader = StdinConfigReader()
|
|
58
|
+
config = await config_reader.read_initial()
|
|
59
|
+
|
|
60
|
+
# Create and start the server
|
|
61
|
+
server = DevHttpPlayerServer(
|
|
62
|
+
port=args.port,
|
|
63
|
+
host=args.host,
|
|
64
|
+
transport_mode=args.transport,
|
|
65
|
+
config=config,
|
|
66
|
+
)
|
|
67
|
+
runner = await server.start()
|
|
68
|
+
|
|
69
|
+
# Set up clean shutdown
|
|
70
|
+
shutdown_event = asyncio.Event()
|
|
71
|
+
loop = asyncio.get_event_loop()
|
|
72
|
+
|
|
73
|
+
def _signal_handler() -> None:
|
|
74
|
+
shutdown_event.set()
|
|
75
|
+
|
|
76
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
77
|
+
loop.add_signal_handler(sig, _signal_handler)
|
|
78
|
+
|
|
79
|
+
# Start watching stdin for config updates in the background
|
|
80
|
+
async def _watch_config() -> None:
|
|
81
|
+
await config_reader.watch(server.update_config)
|
|
82
|
+
# stdin closed = parent process exited, shut down
|
|
83
|
+
shutdown_event.set()
|
|
84
|
+
|
|
85
|
+
watch_task = asyncio.create_task(_watch_config())
|
|
86
|
+
|
|
87
|
+
# Wait for shutdown signal
|
|
88
|
+
await shutdown_event.wait()
|
|
89
|
+
|
|
90
|
+
# Clean up
|
|
91
|
+
watch_task.cancel()
|
|
92
|
+
try:
|
|
93
|
+
await watch_task
|
|
94
|
+
except asyncio.CancelledError:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
await server.shutdown()
|
|
98
|
+
await runner.cleanup()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration reader.
|
|
3
|
+
|
|
4
|
+
Reads newline-delimited JSON from stdin:
|
|
5
|
+
- First line = initial config (blocks until received)
|
|
6
|
+
- Subsequent lines = live config updates
|
|
7
|
+
|
|
8
|
+
The Node CLI writes config objects to our stdin whenever the
|
|
9
|
+
appstrata.config.ts file changes on disk.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Default values matching packages/dev/src/types.ts (DEFAULT_CONTEXT / DEFAULT_LIFECYCLE)
|
|
23
|
+
DEFAULT_CONFIG: dict[str, Any] = {
|
|
24
|
+
"context": {
|
|
25
|
+
"instanceId": "dev-instance",
|
|
26
|
+
"duration": 30,
|
|
27
|
+
"mode": "development",
|
|
28
|
+
"environment": "development",
|
|
29
|
+
"viewportWidth": 1920,
|
|
30
|
+
"viewportHeight": 1080,
|
|
31
|
+
"config": {},
|
|
32
|
+
"resources": [],
|
|
33
|
+
"device": {
|
|
34
|
+
"id": "dev-player",
|
|
35
|
+
"name": "Development Player",
|
|
36
|
+
"type": "web-player",
|
|
37
|
+
"platformName": "AppStrata Dev",
|
|
38
|
+
"locale": "en-US",
|
|
39
|
+
"timezone": "UTC",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
"capabilities": ["storage", "proxy"],
|
|
43
|
+
"lifecycle": {
|
|
44
|
+
"initDelay": 0,
|
|
45
|
+
"showDelay": 50,
|
|
46
|
+
"startDelay": 50,
|
|
47
|
+
"hideDelay": 100,
|
|
48
|
+
"stopDelay": 50,
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class StdinConfigReader:
|
|
54
|
+
"""
|
|
55
|
+
Reads newline-delimited JSON config from stdin.
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
reader = StdinConfigReader()
|
|
59
|
+
initial = await reader.read_initial()
|
|
60
|
+
# then in a background task:
|
|
61
|
+
await reader.watch(on_update_callback)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
self._reader: asyncio.StreamReader | None = None
|
|
66
|
+
self._is_tty = sys.stdin.isatty()
|
|
67
|
+
|
|
68
|
+
async def _ensure_reader(self) -> asyncio.StreamReader | None:
|
|
69
|
+
if self._is_tty:
|
|
70
|
+
return None
|
|
71
|
+
if self._reader is None:
|
|
72
|
+
loop = asyncio.get_event_loop()
|
|
73
|
+
self._reader = asyncio.StreamReader()
|
|
74
|
+
protocol = asyncio.StreamReaderProtocol(self._reader)
|
|
75
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
76
|
+
return self._reader
|
|
77
|
+
|
|
78
|
+
async def read_initial(self) -> dict[str, Any]:
|
|
79
|
+
"""
|
|
80
|
+
Read the first JSON line from stdin.
|
|
81
|
+
|
|
82
|
+
Blocks until a complete line is available. If stdin is a TTY
|
|
83
|
+
(no pipe), returns the default config immediately.
|
|
84
|
+
"""
|
|
85
|
+
reader = await self._ensure_reader()
|
|
86
|
+
if reader is None:
|
|
87
|
+
logger.info("No config pipe detected, using defaults")
|
|
88
|
+
return dict(DEFAULT_CONFIG)
|
|
89
|
+
|
|
90
|
+
line = await reader.readline()
|
|
91
|
+
if not line:
|
|
92
|
+
logger.warning("stdin closed before initial config, using defaults")
|
|
93
|
+
return dict(DEFAULT_CONFIG)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
config = json.loads(line)
|
|
97
|
+
logger.info("Initial config received from stdin")
|
|
98
|
+
return config
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
logger.warning("Invalid JSON on stdin, using defaults")
|
|
101
|
+
return dict(DEFAULT_CONFIG)
|
|
102
|
+
|
|
103
|
+
async def watch(self, on_update: Callable[[dict[str, Any]], None]) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Continuously read lines from stdin and call on_update for each
|
|
106
|
+
valid JSON config object received.
|
|
107
|
+
|
|
108
|
+
Runs until stdin is closed. Must be called AFTER read_initial().
|
|
109
|
+
"""
|
|
110
|
+
reader = await self._ensure_reader()
|
|
111
|
+
if reader is None:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
while True:
|
|
115
|
+
line = await reader.readline()
|
|
116
|
+
if not line:
|
|
117
|
+
logger.info("stdin closed, stopping config watcher")
|
|
118
|
+
break
|
|
119
|
+
try:
|
|
120
|
+
config = json.loads(line)
|
|
121
|
+
logger.info("Config update received from stdin")
|
|
122
|
+
on_update(config)
|
|
123
|
+
except json.JSONDecodeError:
|
|
124
|
+
logger.warning("Invalid JSON on stdin update line, ignoring")
|