create-roxyon-app 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/LICENSE +21 -0
- package/README.md +41 -0
- package/bin/create-roxyon-app.js +2 -0
- package/dist/index.js +471 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BEA Technology Company
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# create-roxyon-app
|
|
2
|
+
|
|
3
|
+
Scaffold a Roxyon project that's ready for `roxyon deploy` and legible to any AI
|
|
4
|
+
assistant.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm create roxyon-app@latest my-app
|
|
8
|
+
# or
|
|
9
|
+
npx create-roxyon-app my-app --template lumen-baas --host my-domain.com
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Templates
|
|
13
|
+
|
|
14
|
+
| id | what |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `lumen` | LumenJS SPA (vanilla JS/HTML/CSS, no build step) |
|
|
17
|
+
| `lumen-baas` | LumenJS SPA wired to the Roxyon BaaS — RX SDK, `rxError()` helper, an auth-free data example (`items.view`) |
|
|
18
|
+
| `node` | Minimal Node.js HTTP server (no deps), reads `PORT`/`HOST` from the environment |
|
|
19
|
+
|
|
20
|
+
LumenJS templates are scaffolded by `@lmjs/cli` under the hood, then overlaid —
|
|
21
|
+
so they always track the current framework.
|
|
22
|
+
|
|
23
|
+
## What every project gets
|
|
24
|
+
|
|
25
|
+
- `roxyon.json` — host, folder, runtime, build command
|
|
26
|
+
- **`AGENTS.md`** — how to build and deploy this project, plus the LumenJS V1 /
|
|
27
|
+
BaaS gotchas. Also written as `CLAUDE.md`, `GEMINI.md`,
|
|
28
|
+
`.cursor/rules/roxyon.mdc`, `.github/copilot-instructions.md`
|
|
29
|
+
- `llms.txt` — pointers to the framework + platform docs
|
|
30
|
+
- `.roxyonignore`, `.gitignore`
|
|
31
|
+
- npm scripts: `dev`, `build`, `deploy`
|
|
32
|
+
|
|
33
|
+
## Flags
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
-t, --template <id> lumen | lumen-baas | node
|
|
37
|
+
--host <domain> deploy host (a domain on your Roxyon subscription)
|
|
38
|
+
--folder <path> sub-path under the host
|
|
39
|
+
--no-install skip npm install
|
|
40
|
+
-y, --yes accept defaults, no prompts
|
|
41
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { basename, resolve as resolve2 } from "path";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
|
|
8
|
+
// src/scaffold.ts
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import { access, mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
11
|
+
import { dirname, join, resolve } from "path";
|
|
12
|
+
import { buildProjectConfig, saveProjectConfig } from "@roxyon/deploy-core";
|
|
13
|
+
|
|
14
|
+
// src/agent-files.ts
|
|
15
|
+
function agentFiles(config, opts) {
|
|
16
|
+
const isLumen = config.runtime === "lumen";
|
|
17
|
+
const dev = isLumen ? "npm run dev" : `(your app: ${config.start ?? "npm run start"})`;
|
|
18
|
+
const build = config.build ? config.build : "(no build step)";
|
|
19
|
+
const agents = `# AGENTS.md
|
|
20
|
+
|
|
21
|
+
Working notes for AI assistants and humans. Also read as \`CLAUDE.md\`,
|
|
22
|
+
\`GEMINI.md\`, \`.cursor/rules/roxyon.mdc\`, \`.github/copilot-instructions.md\`.
|
|
23
|
+
|
|
24
|
+
## This project
|
|
25
|
+
|
|
26
|
+
- **${config.name}** \u2014 a ${config.runtime} project deployed on **Roxyon**.
|
|
27
|
+
- Config: \`roxyon.json\` (host \`${config.host}\`${config.folder ? `, folder \`${config.folder}\`` : ""}).
|
|
28
|
+
- Local dev: \`${dev}\`
|
|
29
|
+
- Build: \`${build}\`
|
|
30
|
+
|
|
31
|
+
${isLumen ? `## LumenJS (V1) \u2014 the rules that bite
|
|
32
|
+
|
|
33
|
+
Vanilla JS/HTML/CSS, **no build step** for the framework. \`.view\` files under
|
|
34
|
+
\`src/views/\` are one per route. Full reference: https://lumenjs.com/llms/lumenjs.md
|
|
35
|
+
|
|
36
|
+
- State is \`var\` (never \`let\`/\`const\`) in a \`<script>\` in the view; reactive
|
|
37
|
+
only when named in \`bind="name"\` on an ancestor element.
|
|
38
|
+
- **Never nest a \`bind\` inside another \`bind\`** \u2014 the inner subtree stops
|
|
39
|
+
rendering, interpolations come out empty.
|
|
40
|
+
- \`bind\` renders its descendants, not the element's own attributes \u2014 put it on
|
|
41
|
+
the parent of whatever interpolates.
|
|
42
|
+
- \`:for\` does not resolve the loop variable in nested loops or in an element's
|
|
43
|
+
own plain attributes. Build that markup as a JS string and inject with
|
|
44
|
+
\`.html()\`. Backtick the moustache on a \`:for\` element's own attribute.
|
|
45
|
+
- A \`{{ fn() }}\` moustache runs once and goes stale \u2014 compute into a \`var\`.
|
|
46
|
+
- Forms: \`o-sub\` / \`b-sub\` on the \`<form>\`. There is no \`v-model\`.
|
|
47
|
+
` : `## Runtime
|
|
48
|
+
|
|
49
|
+
${config.runtime} app. \`PORT\` and \`HOST\` are set by the platform \u2014 read them
|
|
50
|
+
from the environment, never hard-code or override them. The start command is
|
|
51
|
+
\`${config.start ?? "npm run start"}\` (see \`roxyon.json\`).
|
|
52
|
+
`}${opts.baas ? `
|
|
53
|
+
## Roxyon BaaS
|
|
54
|
+
|
|
55
|
+
Configured in \`src/roxyon-baas.js\`. Reference: https://roxyon.com/llms/baas.md
|
|
56
|
+
|
|
57
|
+
- **A failed write returns HTTP 200 with an \`error\` field** \u2014 check for it
|
|
58
|
+
before treating any create/update/delete as successful.
|
|
59
|
+
- The \`in\` operator takes a comma-joined string, not an array.
|
|
60
|
+
- Auth: \`rx.login({Email,Password})\`, \`rx.me()\`, \`rx.logout()\`.
|
|
61
|
+
` : ""}
|
|
62
|
+
## Deploy
|
|
63
|
+
|
|
64
|
+
Do **not** SFTP files or edit them on the server. Deploy with:
|
|
65
|
+
|
|
66
|
+
\`\`\`bash
|
|
67
|
+
roxyon deploy # or the roxyon_deploy MCP tool (dry run, then confirm:true)
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
- Env vars live on the platform: \`roxyon env set KEY=value\`, then redeploy.
|
|
71
|
+
- Git push-to-deploy: \`roxyon link <repo-url>\`.
|
|
72
|
+
- First deploy creates the app automatically.
|
|
73
|
+
`;
|
|
74
|
+
const llmsTxt = `# ${config.name}
|
|
75
|
+
|
|
76
|
+
> A ${config.runtime} project on Roxyon. Deploy with \`roxyon deploy\`.
|
|
77
|
+
|
|
78
|
+
## Docs
|
|
79
|
+
|
|
80
|
+
- [How to build & deploy](AGENTS.md)
|
|
81
|
+
${isLumen ? "- [LumenJS reference](https://lumenjs.com/llms/lumenjs.md)\n" : ""}${opts.baas ? "- [Roxyon BaaS reference](https://roxyon.com/llms/baas.md)\n" : ""}- [Deploying to Roxyon](https://roxyon.com/llms/deploy.md)
|
|
82
|
+
`;
|
|
83
|
+
const cursorRule = `---
|
|
84
|
+
description: How to build and deploy this Roxyon project
|
|
85
|
+
alwaysApply: true
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
${agents}`;
|
|
89
|
+
return [
|
|
90
|
+
{ path: "AGENTS.md", content: agents },
|
|
91
|
+
{ path: "CLAUDE.md", content: "See [AGENTS.md](AGENTS.md).\n" },
|
|
92
|
+
{ path: "GEMINI.md", content: "See [AGENTS.md](AGENTS.md).\n" },
|
|
93
|
+
{ path: "llms.txt", content: llmsTxt },
|
|
94
|
+
{ path: ".cursor/rules/roxyon.mdc", content: cursorRule },
|
|
95
|
+
{ path: ".github/copilot-instructions.md", content: agents },
|
|
96
|
+
{
|
|
97
|
+
path: ".roxyonignore",
|
|
98
|
+
content: ["node_modules", ".git", ".next", ".venv", "dist/.cache", ".DS_Store", ""].join(
|
|
99
|
+
"\n"
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/templates.ts
|
|
106
|
+
var TEMPLATES = {
|
|
107
|
+
lumen: { id: "lumen", label: "LumenJS SPA", runtime: "lumen", baas: false, viaLmjs: true },
|
|
108
|
+
"lumen-baas": {
|
|
109
|
+
id: "lumen-baas",
|
|
110
|
+
label: "LumenJS SPA + Roxyon BaaS (auth + data example)",
|
|
111
|
+
runtime: "lumen",
|
|
112
|
+
baas: true,
|
|
113
|
+
viaLmjs: true
|
|
114
|
+
},
|
|
115
|
+
node: {
|
|
116
|
+
id: "node",
|
|
117
|
+
label: "Node.js app (minimal HTTP server)",
|
|
118
|
+
runtime: "node",
|
|
119
|
+
baas: false,
|
|
120
|
+
viaLmjs: false
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function nodeTemplateFiles(name) {
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
path: "package.json",
|
|
127
|
+
content: `${JSON.stringify(
|
|
128
|
+
{
|
|
129
|
+
name,
|
|
130
|
+
version: "0.1.0",
|
|
131
|
+
private: true,
|
|
132
|
+
type: "module",
|
|
133
|
+
scripts: {
|
|
134
|
+
start: "node src/server.js",
|
|
135
|
+
dev: "node --watch src/server.js"
|
|
136
|
+
},
|
|
137
|
+
engines: { node: ">=20" }
|
|
138
|
+
},
|
|
139
|
+
null,
|
|
140
|
+
2
|
|
141
|
+
)}
|
|
142
|
+
`
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
path: "src/server.js",
|
|
146
|
+
content: `import { createServer } from 'node:http';
|
|
147
|
+
|
|
148
|
+
// The platform sets PORT and HOST. Never hard-code or override them.
|
|
149
|
+
const port = Number(process.env.PORT) || 3000;
|
|
150
|
+
const host = process.env.HOST || '0.0.0.0';
|
|
151
|
+
|
|
152
|
+
const server = createServer((req, res) => {
|
|
153
|
+
if (req.url === '/health') {
|
|
154
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
155
|
+
res.end('ok');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
159
|
+
res.end(JSON.stringify({ hello: 'from ${name} on Roxyon', url: req.url }));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
server.listen(port, host, () => {
|
|
163
|
+
console.log(\`listening on http://\${host}:\${port}\`);
|
|
164
|
+
});
|
|
165
|
+
`
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
path: "README.md",
|
|
169
|
+
content: `# ${name}
|
|
170
|
+
|
|
171
|
+
A Node.js app on Roxyon.
|
|
172
|
+
|
|
173
|
+
\`\`\`bash
|
|
174
|
+
npm install
|
|
175
|
+
npm run dev # local, http://localhost:3000
|
|
176
|
+
roxyon deploy # to Roxyon (first run creates the app)
|
|
177
|
+
\`\`\`
|
|
178
|
+
|
|
179
|
+
See [AGENTS.md](AGENTS.md).
|
|
180
|
+
`
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
function baasOverlayFiles() {
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
path: "src/roxyon-baas.js",
|
|
188
|
+
content: `// Roxyon BaaS client. The RX SDK is loaded in index.html from the CDN.
|
|
189
|
+
//
|
|
190
|
+
// A FAILED WRITE RESOLVES with an { error } field (top-level or inside
|
|
191
|
+
// results[]) \u2014 it does NOT reject. Always run rxError() on a write response.
|
|
192
|
+
window.rx = new RX({
|
|
193
|
+
'Application-ID': 'YOUR_APPLICATION_ID',
|
|
194
|
+
'JavaScript-KEY': 'YOUR_JAVASCRIPT_KEY',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
window.rxError = function (r) {
|
|
198
|
+
if (!r) return 'No response from the server.';
|
|
199
|
+
if (r.error) return r.error;
|
|
200
|
+
var rows = r.results;
|
|
201
|
+
if (Object.prototype.toString.call(rows) === '[object Array]') {
|
|
202
|
+
for (var i = 0; i < rows.length; i++) if (rows[i] && rows[i].error) return rows[i].error;
|
|
203
|
+
}
|
|
204
|
+
return '';
|
|
205
|
+
};
|
|
206
|
+
`
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
path: "src/views/items.view",
|
|
210
|
+
content: `<div class="ph50 pv30" bind="items">
|
|
211
|
+
<div class="fs20 b mb20">Items ({{items.length}})</div>
|
|
212
|
+
<form o-sub="addItem" class="mb20">
|
|
213
|
+
<input name="Title" placeholder="New item" required class="p10" />
|
|
214
|
+
<button type="submit" class="p10">Add</button>
|
|
215
|
+
</form>
|
|
216
|
+
<div :if="msg != ''" class="cr mb10">{{msg}}</div>
|
|
217
|
+
<div :for="items as it">
|
|
218
|
+
<div class="pv5">{{it.Title}}</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<script>
|
|
223
|
+
// 'var' \u2014 not let/const. Reactive because it's named in bind="items".
|
|
224
|
+
var items = [];
|
|
225
|
+
var msg = '';
|
|
226
|
+
|
|
227
|
+
function loadItems() {
|
|
228
|
+
rx.get('/Items', { fields: 'objectId,Title', order: '-createdAt', limit: 50 }).then(function (r) {
|
|
229
|
+
var err = rxError(r);
|
|
230
|
+
if (err) { msg = err; return; }
|
|
231
|
+
items = r.results || [];
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function addItem(f, d) {
|
|
236
|
+
msg = '';
|
|
237
|
+
rx.post('/Items', { Title: d.Title }).then(function (r) {
|
|
238
|
+
var err = rxError(r); // a failed write still lands here
|
|
239
|
+
if (err) { msg = err; return; }
|
|
240
|
+
f.find('[name="Title"]').val('');
|
|
241
|
+
loadItems();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
loadItems();
|
|
246
|
+
</script>
|
|
247
|
+
|
|
248
|
+
<settings> { hasFooter: false, hasHeader: false, hasNav: false } </settings>
|
|
249
|
+
`
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/scaffold.ts
|
|
255
|
+
function run(cmd, args, cwd) {
|
|
256
|
+
return new Promise((res) => {
|
|
257
|
+
const child = spawn(cmd, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
258
|
+
child.on("close", (code) => res(code ?? 1));
|
|
259
|
+
child.on("error", () => res(127));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async function writeFileOut(dir, f) {
|
|
263
|
+
const path = join(dir, f.path);
|
|
264
|
+
await mkdir(dirname(path), { recursive: true });
|
|
265
|
+
await writeFile(path, f.content);
|
|
266
|
+
}
|
|
267
|
+
async function exists(path) {
|
|
268
|
+
try {
|
|
269
|
+
await access(path);
|
|
270
|
+
return true;
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function isEmptyish(dir) {
|
|
276
|
+
try {
|
|
277
|
+
const entries = await readdir(dir);
|
|
278
|
+
return entries.filter((e) => e !== ".git" && e !== ".DS_Store").length === 0;
|
|
279
|
+
} catch {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function patchPackageJson(dir, name, patch) {
|
|
284
|
+
const path = join(dir, "package.json");
|
|
285
|
+
let pkg = {};
|
|
286
|
+
try {
|
|
287
|
+
pkg = JSON.parse(await readFile(path, "utf8"));
|
|
288
|
+
} catch {
|
|
289
|
+
pkg = { name, version: "0.1.0", private: true };
|
|
290
|
+
}
|
|
291
|
+
pkg.scripts = { ...pkg.scripts, ...patch.scripts };
|
|
292
|
+
if (patch.devDependencies) {
|
|
293
|
+
pkg.devDependencies = { ...pkg.devDependencies, ...patch.devDependencies };
|
|
294
|
+
}
|
|
295
|
+
await writeFile(path, `${JSON.stringify(pkg, null, 2)}
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
async function scaffold(input) {
|
|
299
|
+
const dir = resolve(input.dir);
|
|
300
|
+
if (!await isEmptyish(dir)) {
|
|
301
|
+
throw new Error(`${dir} is not empty.`);
|
|
302
|
+
}
|
|
303
|
+
await mkdir(dir, { recursive: true });
|
|
304
|
+
const config = buildProjectConfig({
|
|
305
|
+
cwd: dir,
|
|
306
|
+
name: input.name,
|
|
307
|
+
runtime: input.template.runtime,
|
|
308
|
+
host: input.host,
|
|
309
|
+
folder: input.folder
|
|
310
|
+
});
|
|
311
|
+
if (input.template.viaLmjs) {
|
|
312
|
+
const code = await run("npx", ["--yes", "@lmjs/cli", "create", input.name], dir);
|
|
313
|
+
if (code !== 0 || !await exists(join(dir, "src", "index.html"))) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
"Could not scaffold the LumenJS skeleton (npx @lmjs/cli create failed). Is npm online?"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
await patchPackageJson(dir, input.name, {
|
|
319
|
+
scripts: {
|
|
320
|
+
dev: "lm serve",
|
|
321
|
+
build: "lm build --serverless",
|
|
322
|
+
deploy: "lm build --serverless && roxyon deploy"
|
|
323
|
+
},
|
|
324
|
+
devDependencies: { "@lmjs/cli": "^1.0.51" }
|
|
325
|
+
});
|
|
326
|
+
if (input.template.baas) {
|
|
327
|
+
for (const f of baasOverlayFiles()) await writeFileOut(dir, f);
|
|
328
|
+
const htmlPath = join(dir, "src", "index.html");
|
|
329
|
+
try {
|
|
330
|
+
const html = await readFile(htmlPath, "utf8");
|
|
331
|
+
if (!html.includes("rxjs")) {
|
|
332
|
+
await writeFile(
|
|
333
|
+
htmlPath,
|
|
334
|
+
html.replace(
|
|
335
|
+
'<script src="/js/bea.js"',
|
|
336
|
+
'<script src="https://cdn.roxyon.com/libs/rxjs/1.0.0/rx.js"></script>\n <script src="/roxyon-baas.js"></script>\n <script src="/js/bea.js"'
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
for (const f of nodeTemplateFiles(input.name)) await writeFileOut(dir, f);
|
|
345
|
+
}
|
|
346
|
+
for (const f of agentFiles(config, { baas: input.template.baas })) {
|
|
347
|
+
await writeFileOut(dir, f);
|
|
348
|
+
}
|
|
349
|
+
await saveProjectConfig(config, dir);
|
|
350
|
+
const gitignore = join(dir, ".gitignore");
|
|
351
|
+
const existing = await readFile(gitignore, "utf8").catch(() => "");
|
|
352
|
+
if (!existing.includes("node_modules")) {
|
|
353
|
+
await writeFile(gitignore, `${existing}${existing ? "\n" : ""}node_modules
|
|
354
|
+
dist
|
|
355
|
+
.DS_Store
|
|
356
|
+
`);
|
|
357
|
+
}
|
|
358
|
+
let installed = false;
|
|
359
|
+
if (input.install) {
|
|
360
|
+
const code = await run("npm", ["install"], dir);
|
|
361
|
+
installed = code === 0;
|
|
362
|
+
}
|
|
363
|
+
return { dir, config, installed };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/index.ts
|
|
367
|
+
function parseArgs(argv) {
|
|
368
|
+
const a = { install: true, yes: false };
|
|
369
|
+
for (let i = 0; i < argv.length; i++) {
|
|
370
|
+
const arg = argv[i];
|
|
371
|
+
if (arg === "--no-install") a.install = false;
|
|
372
|
+
else if (arg === "-y" || arg === "--yes") a.yes = true;
|
|
373
|
+
else if (arg === "--template" || arg === "-t") a.template = argv[++i];
|
|
374
|
+
else if (arg === "--host") a.host = argv[++i];
|
|
375
|
+
else if (arg === "--folder") a.folder = argv[++i];
|
|
376
|
+
else if (arg === "--help" || arg === "-h") a.dir = "--help";
|
|
377
|
+
else if (!arg.startsWith("-") && !a.dir) a.dir = arg;
|
|
378
|
+
}
|
|
379
|
+
return a;
|
|
380
|
+
}
|
|
381
|
+
var HELP = `${pc.bold("create-roxyon-app")} \u2014 scaffold a Roxyon project
|
|
382
|
+
|
|
383
|
+
npm create roxyon-app@latest my-app
|
|
384
|
+
npx create-roxyon-app my-app --template lumen-baas
|
|
385
|
+
|
|
386
|
+
Options:
|
|
387
|
+
-t, --template <id> ${Object.keys(TEMPLATES).join(" | ")}
|
|
388
|
+
--host <domain> deploy host (a domain on your Roxyon subscription)
|
|
389
|
+
--folder <path> sub-path under the host
|
|
390
|
+
--no-install skip npm install
|
|
391
|
+
-y, --yes accept defaults, no prompts
|
|
392
|
+
`;
|
|
393
|
+
async function ask(fn) {
|
|
394
|
+
const v = await fn();
|
|
395
|
+
if (p.isCancel(v)) {
|
|
396
|
+
p.cancel("Cancelled.");
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
return v;
|
|
400
|
+
}
|
|
401
|
+
async function main() {
|
|
402
|
+
const args = parseArgs(process.argv.slice(2));
|
|
403
|
+
if (args.dir === "--help") {
|
|
404
|
+
process.stdout.write(HELP);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
p.intro(pc.bgYellow(pc.black(" create-roxyon-app ")));
|
|
408
|
+
const dirInput = args.dir ?? (args.yes ? "roxyon-app" : await ask(
|
|
409
|
+
() => p.text({ message: "Project directory", placeholder: "my-app", defaultValue: "my-app" })
|
|
410
|
+
));
|
|
411
|
+
const dir = resolve2(dirInput);
|
|
412
|
+
const name = basename(dir);
|
|
413
|
+
const templateId = args.template ?? (args.yes ? "lumen" : await ask(
|
|
414
|
+
() => p.select({
|
|
415
|
+
message: "Template",
|
|
416
|
+
initialValue: "lumen",
|
|
417
|
+
options: Object.values(TEMPLATES).map((t) => ({ value: t.id, label: t.label }))
|
|
418
|
+
})
|
|
419
|
+
));
|
|
420
|
+
const template = TEMPLATES[templateId];
|
|
421
|
+
if (!template) {
|
|
422
|
+
p.cancel(`Unknown template "${templateId}". Choose: ${Object.keys(TEMPLATES).join(", ")}`);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
const host = args.host ?? (args.yes ? "" : await ask(
|
|
426
|
+
() => p.text({
|
|
427
|
+
message: "Deploy host (a domain on your Roxyon subscription \u2014 leave blank to set later)",
|
|
428
|
+
placeholder: "example.com",
|
|
429
|
+
defaultValue: ""
|
|
430
|
+
})
|
|
431
|
+
));
|
|
432
|
+
const folder = args.folder ?? (args.yes || template.runtime === "lumen" ? "" : await ask(
|
|
433
|
+
() => p.text({ message: "Folder under the host", placeholder: name, defaultValue: name })
|
|
434
|
+
));
|
|
435
|
+
const s = p.spinner();
|
|
436
|
+
s.start("Scaffolding");
|
|
437
|
+
try {
|
|
438
|
+
const result = await scaffold({
|
|
439
|
+
dir,
|
|
440
|
+
name,
|
|
441
|
+
template,
|
|
442
|
+
host: host || "example.com",
|
|
443
|
+
folder,
|
|
444
|
+
install: args.install
|
|
445
|
+
});
|
|
446
|
+
s.stop("Scaffolded.");
|
|
447
|
+
const rel = dir.startsWith(process.cwd()) ? `.${dir.slice(process.cwd().length)}` : dir;
|
|
448
|
+
const steps = [
|
|
449
|
+
`cd ${rel}`,
|
|
450
|
+
...result.installed ? [] : ["npm install"],
|
|
451
|
+
template.runtime === "lumen" ? "npm run dev" : "npm run dev",
|
|
452
|
+
...host ? [] : [pc.dim('# set "host" in roxyon.json (or run `roxyon init`)')],
|
|
453
|
+
"roxyon login",
|
|
454
|
+
"roxyon deploy"
|
|
455
|
+
];
|
|
456
|
+
p.note(steps.join("\n"), "Next");
|
|
457
|
+
p.outro(
|
|
458
|
+
`${pc.green("Ready.")} ${template.baas ? "Set your BaaS keys in src/roxyon-baas.js. " : ""}AGENTS.md tells any AI assistant how to build & deploy this.`
|
|
459
|
+
);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
s.stop("Failed.");
|
|
462
|
+
p.cancel(err instanceof Error ? err.message : String(err));
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
main().catch((err) => {
|
|
467
|
+
process.stderr.write(`${err instanceof Error ? err.stack : String(err)}
|
|
468
|
+
`);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
});
|
|
471
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/scaffold.ts","../src/agent-files.ts","../src/templates.ts"],"sourcesContent":["import { basename, resolve } from 'node:path';\nimport * as p from '@clack/prompts';\nimport pc from 'picocolors';\nimport { scaffold } from './scaffold.js';\nimport { TEMPLATES, type TemplateId } from './templates.js';\n\ninterface Args {\n dir?: string;\n template?: TemplateId;\n host?: string;\n folder?: string;\n install: boolean;\n yes: boolean;\n}\n\nfunction parseArgs(argv: string[]): Args {\n const a: Args = { install: true, yes: false };\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]!;\n if (arg === '--no-install') a.install = false;\n else if (arg === '-y' || arg === '--yes') a.yes = true;\n else if (arg === '--template' || arg === '-t') a.template = argv[++i] as TemplateId;\n else if (arg === '--host') a.host = argv[++i];\n else if (arg === '--folder') a.folder = argv[++i];\n else if (arg === '--help' || arg === '-h') a.dir = '--help';\n else if (!arg.startsWith('-') && !a.dir) a.dir = arg;\n }\n return a;\n}\n\nconst HELP = `${pc.bold('create-roxyon-app')} — scaffold a Roxyon project\n\n npm create roxyon-app@latest my-app\n npx create-roxyon-app my-app --template lumen-baas\n\nOptions:\n -t, --template <id> ${Object.keys(TEMPLATES).join(' | ')}\n --host <domain> deploy host (a domain on your Roxyon subscription)\n --folder <path> sub-path under the host\n --no-install skip npm install\n -y, --yes accept defaults, no prompts\n`;\n\nasync function ask<T>(fn: () => Promise<T | symbol>): Promise<T> {\n const v = await fn();\n if (p.isCancel(v)) {\n p.cancel('Cancelled.');\n process.exit(0);\n }\n return v as T;\n}\n\nasync function main(): Promise<void> {\n const args = parseArgs(process.argv.slice(2));\n if (args.dir === '--help') {\n process.stdout.write(HELP);\n return;\n }\n\n p.intro(pc.bgYellow(pc.black(' create-roxyon-app ')));\n\n const dirInput =\n args.dir ??\n (args.yes\n ? 'roxyon-app'\n : await ask<string>(() =>\n p.text({ message: 'Project directory', placeholder: 'my-app', defaultValue: 'my-app' }),\n ));\n const dir = resolve(dirInput);\n const name = basename(dir);\n\n const templateId: TemplateId =\n args.template ??\n (args.yes\n ? 'lumen'\n : await ask<TemplateId>(() =>\n p.select({\n message: 'Template',\n initialValue: 'lumen' as TemplateId,\n options: Object.values(TEMPLATES).map((t) => ({ value: t.id, label: t.label })),\n }),\n ));\n const template = TEMPLATES[templateId];\n if (!template) {\n p.cancel(`Unknown template \"${templateId}\". Choose: ${Object.keys(TEMPLATES).join(', ')}`);\n process.exit(1);\n }\n\n const host =\n args.host ??\n (args.yes\n ? ''\n : await ask<string>(() =>\n p.text({\n message:\n 'Deploy host (a domain on your Roxyon subscription — leave blank to set later)',\n placeholder: 'example.com',\n defaultValue: '',\n }),\n ));\n\n const folder =\n args.folder ??\n (args.yes || template.runtime === 'lumen'\n ? ''\n : await ask<string>(() =>\n p.text({ message: 'Folder under the host', placeholder: name, defaultValue: name }),\n ));\n\n const s = p.spinner();\n s.start('Scaffolding');\n try {\n const result = await scaffold({\n dir,\n name,\n template,\n host: host || 'example.com',\n folder,\n install: args.install,\n });\n s.stop('Scaffolded.');\n\n const rel = dir.startsWith(process.cwd()) ? `.${dir.slice(process.cwd().length)}` : dir;\n const steps = [\n `cd ${rel}`,\n ...(result.installed ? [] : ['npm install']),\n template.runtime === 'lumen' ? 'npm run dev' : 'npm run dev',\n ...(host ? [] : [pc.dim('# set \"host\" in roxyon.json (or run `roxyon init`)')]),\n 'roxyon login',\n 'roxyon deploy',\n ];\n p.note(steps.join('\\n'), 'Next');\n p.outro(\n `${pc.green('Ready.')} ${template.baas ? 'Set your BaaS keys in src/roxyon-baas.js. ' : ''}AGENTS.md tells any AI assistant how to build & deploy this.`,\n );\n } catch (err) {\n s.stop('Failed.');\n p.cancel(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`${err instanceof Error ? err.stack : String(err)}\\n`);\n process.exit(1);\n});\n","import { spawn } from 'node:child_process';\nimport { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { type ProjectConfig, buildProjectConfig, saveProjectConfig } from '@roxyon/deploy-core';\nimport { type FileOut, agentFiles } from './agent-files.js';\nimport { type Template, baasOverlayFiles, nodeTemplateFiles } from './templates.js';\n\nexport interface ScaffoldInput {\n dir: string;\n name: string;\n template: Template;\n host: string;\n folder: string;\n install: boolean;\n}\n\nexport interface ScaffoldResult {\n dir: string;\n config: ProjectConfig;\n installed: boolean;\n}\n\nfunction run(cmd: string, args: string[], cwd: string): Promise<number> {\n return new Promise((res) => {\n const child = spawn(cmd, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });\n child.on('close', (code) => res(code ?? 1));\n child.on('error', () => res(127));\n });\n}\n\nasync function writeFileOut(dir: string, f: FileOut): Promise<void> {\n const path = join(dir, f.path);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, f.content);\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function isEmptyish(dir: string): Promise<boolean> {\n try {\n const entries = await readdir(dir);\n return entries.filter((e) => e !== '.git' && e !== '.DS_Store').length === 0;\n } catch {\n return true; // does not exist yet\n }\n}\n\n/** Merge `scripts` and `devDependencies` into a package.json, seeding one if absent. */\nasync function patchPackageJson(\n dir: string,\n name: string,\n patch: { scripts?: Record<string, string>; devDependencies?: Record<string, string> },\n): Promise<void> {\n const path = join(dir, 'package.json');\n let pkg: Record<string, unknown> = {};\n try {\n pkg = JSON.parse(await readFile(path, 'utf8'));\n } catch {\n pkg = { name, version: '0.1.0', private: true };\n }\n pkg.scripts = { ...(pkg.scripts as object), ...patch.scripts };\n if (patch.devDependencies) {\n pkg.devDependencies = { ...(pkg.devDependencies as object), ...patch.devDependencies };\n }\n await writeFile(path, `${JSON.stringify(pkg, null, 2)}\\n`);\n}\n\nexport async function scaffold(input: ScaffoldInput): Promise<ScaffoldResult> {\n const dir = resolve(input.dir);\n if (!(await isEmptyish(dir))) {\n throw new Error(`${dir} is not empty.`);\n }\n await mkdir(dir, { recursive: true });\n\n const config = buildProjectConfig({\n cwd: dir,\n name: input.name,\n runtime: input.template.runtime,\n host: input.host,\n folder: input.folder,\n });\n\n // ---- framework skeleton ----\n if (input.template.viaLmjs) {\n // `lm create <name>` fills the CURRENT directory (the name is only used for\n // [ProjectName] substitution, not as a subdirectory).\n const code = await run('npx', ['--yes', '@lmjs/cli', 'create', input.name], dir);\n if (code !== 0 || !(await exists(join(dir, 'src', 'index.html')))) {\n throw new Error(\n 'Could not scaffold the LumenJS skeleton (npx @lmjs/cli create failed). Is npm online?',\n );\n }\n\n await patchPackageJson(dir, input.name, {\n scripts: {\n dev: 'lm serve',\n build: 'lm build --serverless',\n deploy: 'lm build --serverless && roxyon deploy',\n },\n devDependencies: { '@lmjs/cli': '^1.0.51' },\n });\n\n if (input.template.baas) {\n for (const f of baasOverlayFiles()) await writeFileOut(dir, f);\n // Load the RX SDK + our client before index.js.\n const htmlPath = join(dir, 'src', 'index.html');\n try {\n const html = await readFile(htmlPath, 'utf8');\n if (!html.includes('rxjs')) {\n await writeFile(\n htmlPath,\n html.replace(\n '<script src=\"/js/bea.js\"',\n '<script src=\"https://cdn.roxyon.com/libs/rxjs/1.0.0/rx.js\"></script>\\n ' +\n '<script src=\"/roxyon-baas.js\"></script>\\n <script src=\"/js/bea.js\"',\n ),\n );\n }\n } catch {\n /* template shape changed upstream — the file is still copied, just not linked */\n }\n }\n } else {\n for (const f of nodeTemplateFiles(input.name)) await writeFileOut(dir, f);\n }\n\n // ---- Roxyon overlay (every template) ----\n for (const f of agentFiles(config, { baas: input.template.baas })) {\n await writeFileOut(dir, f);\n }\n await saveProjectConfig(config, dir);\n\n // ---- .gitignore ----\n const gitignore = join(dir, '.gitignore');\n const existing = await readFile(gitignore, 'utf8').catch(() => '');\n if (!existing.includes('node_modules')) {\n await writeFile(gitignore, `${existing}${existing ? '\\n' : ''}node_modules\\ndist\\n.DS_Store\\n`);\n }\n\n // ---- install ----\n let installed = false;\n if (input.install) {\n const code = await run('npm', ['install'], dir);\n installed = code === 0;\n }\n\n return { dir, config, installed };\n}\n","import type { ProjectConfig } from '@roxyon/deploy-core';\n\nexport interface FileOut {\n path: string;\n content: string;\n}\n\n/**\n * The files every scaffolded project gets on top of its framework skeleton, so\n * any AI assistant — and any human — knows how to build and deploy it.\n */\nexport function agentFiles(config: ProjectConfig, opts: { baas: boolean }): FileOut[] {\n const isLumen = config.runtime === 'lumen';\n const dev = isLumen ? 'npm run dev' : `(your app: ${config.start ?? 'npm run start'})`;\n const build = config.build ? config.build : '(no build step)';\n\n const agents = `# AGENTS.md\n\nWorking notes for AI assistants and humans. Also read as \\`CLAUDE.md\\`,\n\\`GEMINI.md\\`, \\`.cursor/rules/roxyon.mdc\\`, \\`.github/copilot-instructions.md\\`.\n\n## This project\n\n- **${config.name}** — a ${config.runtime} project deployed on **Roxyon**.\n- Config: \\`roxyon.json\\` (host \\`${config.host}\\`${config.folder ? `, folder \\`${config.folder}\\`` : ''}).\n- Local dev: \\`${dev}\\`\n- Build: \\`${build}\\`\n\n${\n isLumen\n ? `## LumenJS (V1) — the rules that bite\n\nVanilla JS/HTML/CSS, **no build step** for the framework. \\`.view\\` files under\n\\`src/views/\\` are one per route. Full reference: https://lumenjs.com/llms/lumenjs.md\n\n- State is \\`var\\` (never \\`let\\`/\\`const\\`) in a \\`<script>\\` in the view; reactive\n only when named in \\`bind=\"name\"\\` on an ancestor element.\n- **Never nest a \\`bind\\` inside another \\`bind\\`** — the inner subtree stops\n rendering, interpolations come out empty.\n- \\`bind\\` renders its descendants, not the element's own attributes — put it on\n the parent of whatever interpolates.\n- \\`:for\\` does not resolve the loop variable in nested loops or in an element's\n own plain attributes. Build that markup as a JS string and inject with\n \\`.html()\\`. Backtick the moustache on a \\`:for\\` element's own attribute.\n- A \\`{{ fn() }}\\` moustache runs once and goes stale — compute into a \\`var\\`.\n- Forms: \\`o-sub\\` / \\`b-sub\\` on the \\`<form>\\`. There is no \\`v-model\\`.\n`\n : `## Runtime\n\n${config.runtime} app. \\`PORT\\` and \\`HOST\\` are set by the platform — read them\nfrom the environment, never hard-code or override them. The start command is\n\\`${config.start ?? 'npm run start'}\\` (see \\`roxyon.json\\`).\n`\n}${\n opts.baas\n ? `\n## Roxyon BaaS\n\nConfigured in \\`src/roxyon-baas.js\\`. Reference: https://roxyon.com/llms/baas.md\n\n- **A failed write returns HTTP 200 with an \\`error\\` field** — check for it\n before treating any create/update/delete as successful.\n- The \\`in\\` operator takes a comma-joined string, not an array.\n- Auth: \\`rx.login({Email,Password})\\`, \\`rx.me()\\`, \\`rx.logout()\\`.\n`\n : ''\n}\n## Deploy\n\nDo **not** SFTP files or edit them on the server. Deploy with:\n\n\\`\\`\\`bash\nroxyon deploy # or the roxyon_deploy MCP tool (dry run, then confirm:true)\n\\`\\`\\`\n\n- Env vars live on the platform: \\`roxyon env set KEY=value\\`, then redeploy.\n- Git push-to-deploy: \\`roxyon link <repo-url>\\`.\n- First deploy creates the app automatically.\n`;\n\n const llmsTxt = `# ${config.name}\n\n> A ${config.runtime} project on Roxyon. Deploy with \\`roxyon deploy\\`.\n\n## Docs\n\n- [How to build & deploy](AGENTS.md)\n${isLumen ? '- [LumenJS reference](https://lumenjs.com/llms/lumenjs.md)\\n' : ''}${opts.baas ? '- [Roxyon BaaS reference](https://roxyon.com/llms/baas.md)\\n' : ''}- [Deploying to Roxyon](https://roxyon.com/llms/deploy.md)\n`;\n\n const cursorRule = `---\ndescription: How to build and deploy this Roxyon project\nalwaysApply: true\n---\n\n${agents}`;\n\n return [\n { path: 'AGENTS.md', content: agents },\n { path: 'CLAUDE.md', content: 'See [AGENTS.md](AGENTS.md).\\n' },\n { path: 'GEMINI.md', content: 'See [AGENTS.md](AGENTS.md).\\n' },\n { path: 'llms.txt', content: llmsTxt },\n { path: '.cursor/rules/roxyon.mdc', content: cursorRule },\n { path: '.github/copilot-instructions.md', content: agents },\n {\n path: '.roxyonignore',\n content: ['node_modules', '.git', '.next', '.venv', 'dist/.cache', '.DS_Store', ''].join(\n '\\n',\n ),\n },\n ];\n}\n","import type { FileOut } from './agent-files.js';\n\nexport type TemplateId = 'lumen' | 'lumen-baas' | 'node';\n\nexport interface Template {\n id: TemplateId;\n label: string;\n runtime: 'lumen' | 'node';\n baas: boolean;\n /** LumenJS templates are scaffolded by @lmjs/cli, then overlaid. */\n viaLmjs: boolean;\n}\n\nexport const TEMPLATES: Record<TemplateId, Template> = {\n lumen: { id: 'lumen', label: 'LumenJS SPA', runtime: 'lumen', baas: false, viaLmjs: true },\n 'lumen-baas': {\n id: 'lumen-baas',\n label: 'LumenJS SPA + Roxyon BaaS (auth + data example)',\n runtime: 'lumen',\n baas: true,\n viaLmjs: true,\n },\n node: {\n id: 'node',\n label: 'Node.js app (minimal HTTP server)',\n runtime: 'node',\n baas: false,\n viaLmjs: false,\n },\n};\n\n// ---------------------------------------------------------------------------\n// Node template — bundled inline (no framework, no deps).\n// ---------------------------------------------------------------------------\n\nexport function nodeTemplateFiles(name: string): FileOut[] {\n return [\n {\n path: 'package.json',\n content: `${JSON.stringify(\n {\n name,\n version: '0.1.0',\n private: true,\n type: 'module',\n scripts: {\n start: 'node src/server.js',\n dev: 'node --watch src/server.js',\n },\n engines: { node: '>=20' },\n },\n null,\n 2,\n )}\\n`,\n },\n {\n path: 'src/server.js',\n content: `import { createServer } from 'node:http';\n\n// The platform sets PORT and HOST. Never hard-code or override them.\nconst port = Number(process.env.PORT) || 3000;\nconst host = process.env.HOST || '0.0.0.0';\n\nconst server = createServer((req, res) => {\n if (req.url === '/health') {\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('ok');\n return;\n }\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ hello: 'from ${name} on Roxyon', url: req.url }));\n});\n\nserver.listen(port, host, () => {\n console.log(\\`listening on http://\\${host}:\\${port}\\`);\n});\n`,\n },\n {\n path: 'README.md',\n content: `# ${name}\n\nA Node.js app on Roxyon.\n\n\\`\\`\\`bash\nnpm install\nnpm run dev # local, http://localhost:3000\nroxyon deploy # to Roxyon (first run creates the app)\n\\`\\`\\`\n\nSee [AGENTS.md](AGENTS.md).\n`,\n },\n ];\n}\n\n// ---------------------------------------------------------------------------\n// LumenJS BaaS overlay — added on top of the @lmjs/cli skeleton.\n// ---------------------------------------------------------------------------\n\nexport function baasOverlayFiles(): FileOut[] {\n return [\n {\n path: 'src/roxyon-baas.js',\n content: `// Roxyon BaaS client. The RX SDK is loaded in index.html from the CDN.\n//\n// A FAILED WRITE RESOLVES with an { error } field (top-level or inside\n// results[]) — it does NOT reject. Always run rxError() on a write response.\nwindow.rx = new RX({\n 'Application-ID': 'YOUR_APPLICATION_ID',\n 'JavaScript-KEY': 'YOUR_JAVASCRIPT_KEY',\n});\n\nwindow.rxError = function (r) {\n if (!r) return 'No response from the server.';\n if (r.error) return r.error;\n var rows = r.results;\n if (Object.prototype.toString.call(rows) === '[object Array]') {\n for (var i = 0; i < rows.length; i++) if (rows[i] && rows[i].error) return rows[i].error;\n }\n return '';\n};\n`,\n },\n {\n path: 'src/views/items.view',\n content: `<div class=\"ph50 pv30\" bind=\"items\">\n <div class=\"fs20 b mb20\">Items ({{items.length}})</div>\n <form o-sub=\"addItem\" class=\"mb20\">\n <input name=\"Title\" placeholder=\"New item\" required class=\"p10\" />\n <button type=\"submit\" class=\"p10\">Add</button>\n </form>\n <div :if=\"msg != ''\" class=\"cr mb10\">{{msg}}</div>\n <div :for=\"items as it\">\n <div class=\"pv5\">{{it.Title}}</div>\n </div>\n</div>\n\n<script>\n // 'var' — not let/const. Reactive because it's named in bind=\"items\".\n var items = [];\n var msg = '';\n\n function loadItems() {\n rx.get('/Items', { fields: 'objectId,Title', order: '-createdAt', limit: 50 }).then(function (r) {\n var err = rxError(r);\n if (err) { msg = err; return; }\n items = r.results || [];\n });\n }\n\n function addItem(f, d) {\n msg = '';\n rx.post('/Items', { Title: d.Title }).then(function (r) {\n var err = rxError(r); // a failed write still lands here\n if (err) { msg = err; return; }\n f.find('[name=\"Title\"]').val('');\n loadItems();\n });\n }\n\n loadItems();\n</script>\n\n<settings> { hasFooter: false, hasHeader: false, hasNav: false } </settings>\n`,\n },\n ];\n}\n"],"mappings":"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","names":["resolve","resolve"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-roxyon-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a Roxyon project — LumenJS SPA or a Node app — wired for `roxyon deploy`, with AGENTS.md / llms.txt / editor rules so any AI assistant gets it right.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-roxyon-app": "./bin/create-roxyon-app.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"roxyon",
|
|
20
|
+
"lumenjs",
|
|
21
|
+
"scaffold",
|
|
22
|
+
"create",
|
|
23
|
+
"starter"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/bea-technology/roxyon-devtools.git",
|
|
28
|
+
"directory": "packages/create-roxyon-app"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@clack/prompts": "^0.7.0",
|
|
32
|
+
"picocolors": "^1.1.1",
|
|
33
|
+
"@roxyon/deploy-core": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.16.11",
|
|
37
|
+
"tsup": "^8.3.0",
|
|
38
|
+
"typescript": "^5.6.3",
|
|
39
|
+
"vitest": "^2.1.3"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "vitest run"
|
|
45
|
+
}
|
|
46
|
+
}
|