@crewx/app 0.1.0-rc.19 → 0.1.0-rc.20
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/SKILL.md +2 -0
- package/build-app/index.d.mts +3 -0
- package/build-app/index.mjs +57 -7
- package/package.json +2 -2
package/SKILL.md
CHANGED
|
@@ -32,6 +32,8 @@ metadata:
|
|
|
32
32
|
|
|
33
33
|
`crewx app build` needs nothing but the CrewX CLI: it ships the bundler and the SDK that `server.ts` imports as `@crewx/sdk/app`. Run it in the App folder (or pass the folder). The folder needs `server.ts` (default-exports a `CrewXApp` subclass) and a `package.json` with one `bin` name (or `crewx.app.command`); `manifest.json` is created when missing, otherwise only its `_meta["dev.crewx"].actions` is rewritten.
|
|
34
34
|
|
|
35
|
+
Set `crewx.app.env` in `package.json` to `["CREWX_APP_INSTANCE_ID", "CREWX_THREAD_ID"]` (one or both, no repeats, nothing else) when your `@Tool` code reads those values from `process.env`: the build writes the list to every action's `backend.env` and the Host then puts exactly those values in the backend process; without it the backend gets neither and no `env` key is written.
|
|
36
|
+
|
|
35
37
|
stdout is one JSON line with the written `files` and the `actions` found; after a build, stderr says the next command. Exit 0 = built (or in sync with `--check`), 1 = `BUILD_FAILED` (stderr says why) or `BUILD_STALE` (`--check` found a stale file; run `crewx app build`), 2 = usage. `--watch` stays in the foreground and prints one plain line per build until Ctrl-C.
|
|
36
38
|
|
|
37
39
|
## App development
|
package/build-app/index.d.mts
CHANGED
|
@@ -9,6 +9,8 @@ export interface BuildOptions {
|
|
|
9
9
|
export interface AppConfig {
|
|
10
10
|
command: string[];
|
|
11
11
|
external: string[];
|
|
12
|
+
/** crewx.app.env; present only when package.json sets it. */
|
|
13
|
+
env?: string[];
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export interface BuildResult {
|
|
@@ -33,6 +35,7 @@ export function renderActionDeclarations(
|
|
|
33
35
|
imported: { default: unknown; generateManifest: (app: unknown, command: string[]) => { actions?: unknown } },
|
|
34
36
|
command: string[],
|
|
35
37
|
existingText?: string,
|
|
38
|
+
backendEnv?: readonly string[],
|
|
36
39
|
): { fileName: string; text: string };
|
|
37
40
|
export function buildApp(options: {
|
|
38
41
|
appDirectory: string;
|
package/build-app/index.mjs
CHANGED
|
@@ -22,11 +22,16 @@
|
|
|
22
22
|
// Optional app settings in <app>/package.json:
|
|
23
23
|
// "crewx": { "app": {
|
|
24
24
|
// "command": ["sheet", "app"], // skill command the actions run; default: [<the one bin name>, "app"]
|
|
25
|
-
// "external": ["lib/**"]
|
|
26
|
-
//
|
|
25
|
+
// "external": ["lib/**"], // app files (globs relative to the app) kept out of the bundle
|
|
26
|
+
// // and imported at run time from their real place
|
|
27
|
+
// "env": ["CREWX_APP_INSTANCE_ID", "CREWX_THREAD_ID"]
|
|
28
|
+
// // written to every generated action's backend.env, so the Host
|
|
29
|
+
// // puts these values in the process environment; absent = no env key
|
|
30
|
+
// } }
|
|
27
31
|
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, watch, writeFileSync } from 'node:fs';
|
|
32
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, watch, writeFileSync } from 'node:fs';
|
|
29
33
|
import { createRequire } from 'node:module';
|
|
34
|
+
import { tmpdir } from 'node:os';
|
|
30
35
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
31
36
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
37
|
import { CREWX_META_NAMESPACE, convertV2ToManifest } from './manifest.mjs';
|
|
@@ -58,6 +63,10 @@ const SERVER_WRAPPER = [
|
|
|
58
63
|
'',
|
|
59
64
|
].join('\n');
|
|
60
65
|
const ENTRY_WRAPPER = "export { runAppEntry } from '@crewx/sdk/app';\n";
|
|
66
|
+
// The rule the manifest reader applies to backend.env. It is bundled on its own and only imported by
|
|
67
|
+
// the build, so it never reaches dist/server.mjs (and every App without crewx.app.env builds the same bytes).
|
|
68
|
+
const ENV_RULE_WRAPPER = "export { SKILL_BACKEND_ENV_NAMES, isSkillBackendEnv } from '@crewx/sdk/app';\n";
|
|
69
|
+
const ENV_RULE_BUNDLE = 'dist/.env-rule.mjs'; // only names the esbuild output; the file is written under the OS temp folder
|
|
61
70
|
|
|
62
71
|
// The published @crewx/sdk/app is CommonJS. Inlined into an ES module, its require() calls
|
|
63
72
|
// (node built-ins) need a real require.
|
|
@@ -83,9 +92,18 @@ const TSCONFIG_RAW = {
|
|
|
83
92
|
* already on disk. With it only `actions` is replaced, so every other key keeps its value and its
|
|
84
93
|
* place (the file is written back as two-space JSON). Without it the whole file is derived from the
|
|
85
94
|
* App's static definition and starts at release version 0.1.0.
|
|
95
|
+
*
|
|
96
|
+
* `backendEnv` (crewx.app.env, already checked by the caller) is copied to the `backend.env` of every
|
|
97
|
+
* action; left out, no action gets an `env` key.
|
|
86
98
|
*/
|
|
87
|
-
export function renderActionDeclarations(imported, command, existingText) {
|
|
88
|
-
|
|
99
|
+
export function renderActionDeclarations(imported, command, existingText, backendEnv) {
|
|
100
|
+
let generated = imported.generateManifest(imported.default, command);
|
|
101
|
+
if (backendEnv !== undefined && Array.isArray(generated.actions)) {
|
|
102
|
+
generated = {
|
|
103
|
+
...generated,
|
|
104
|
+
actions: generated.actions.map((action) => ({ ...action, backend: { ...action.backend, env: [...backendEnv] } })),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
89
107
|
let manifest;
|
|
90
108
|
if (existingText === undefined) {
|
|
91
109
|
manifest = convertV2ToManifest(generated);
|
|
@@ -140,7 +158,38 @@ export function readAppConfig(appDirectory) {
|
|
|
140
158
|
if (!Array.isArray(external) || !external.every((glob) => typeof glob === 'string')) {
|
|
141
159
|
throw new Error('crewx.app.external must be an array of globs');
|
|
142
160
|
}
|
|
143
|
-
|
|
161
|
+
const env = config.env;
|
|
162
|
+
if (env !== undefined && (!Array.isArray(env) || !env.every((name) => typeof name === 'string'))) {
|
|
163
|
+
throw new Error('crewx.app.env must be an array of environment variable names');
|
|
164
|
+
}
|
|
165
|
+
return { command, external, ...(env === undefined ? {} : { env }) };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Refuses a `crewx.app.env` the manifest reader would refuse, by asking the reader's own rule. */
|
|
169
|
+
async function assertBackendEnv(esbuild, appDirectory, config) {
|
|
170
|
+
let ruleBundle;
|
|
171
|
+
try {
|
|
172
|
+
ruleBundle = await bundle(esbuild, appDirectory, config, {
|
|
173
|
+
contents: ENV_RULE_WRAPPER, sourcefile: 'crewx-app-env-rule.ts', outfile: ENV_RULE_BUNDLE,
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error.errors?.some(({ text }) => /No matching export/u.test(text))) {
|
|
177
|
+
throw new Error('crewx.app.env needs a newer @crewx/sdk: reinstall the CrewX CLI');
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
// The bundle imports nothing from the app, so it runs from the OS temp folder and leaves dist/ alone.
|
|
182
|
+
const temporaryDirectory = mkdtempSync(join(tmpdir(), 'crewx-app-env-'));
|
|
183
|
+
try {
|
|
184
|
+
const temporaryRule = join(temporaryDirectory, 'rule.mjs');
|
|
185
|
+
writeFileSync(temporaryRule, ruleBundle.text, 'utf8');
|
|
186
|
+
const rule = await import(pathToFileURL(temporaryRule).href);
|
|
187
|
+
if (!rule.isSkillBackendEnv(config.env)) {
|
|
188
|
+
throw new Error(`crewx.app.env must be a non-empty list of distinct names from ${rule.SKILL_BACKEND_ENV_NAMES.join(', ')}`);
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
192
|
+
}
|
|
144
193
|
}
|
|
145
194
|
|
|
146
195
|
function deriveCommand(pkg) {
|
|
@@ -296,6 +345,7 @@ export async function buildApp({ appDirectory, check = false, log = () => {} })
|
|
|
296
345
|
if (!existsSync(join(appDirectory, SERVER_ENTRY))) throw new Error(`${join(appDirectory, SERVER_ENTRY)} not found`);
|
|
297
346
|
const config = readAppConfig(appDirectory);
|
|
298
347
|
const esbuild = await loadEsbuild(appDirectory);
|
|
348
|
+
if (config.env !== undefined) await assertBackendEnv(esbuild, appDirectory, config);
|
|
299
349
|
const distDirectory = join(appDirectory, 'dist');
|
|
300
350
|
const distDirectoryExisted = existsSync(distDirectory);
|
|
301
351
|
const temporaryBundle = join(distDirectory, `.server-build-${process.pid}.mjs`);
|
|
@@ -317,7 +367,7 @@ export async function buildApp({ appDirectory, check = false, log = () => {} })
|
|
|
317
367
|
}
|
|
318
368
|
const manifestPath = join(appDirectory, MANIFEST_FILE);
|
|
319
369
|
const existingManifest = existsSync(manifestPath) ? readFileSync(manifestPath, 'utf8') : undefined;
|
|
320
|
-
const declarations = renderActionDeclarations(imported, config.command, existingManifest);
|
|
370
|
+
const declarations = renderActionDeclarations(imported, config.command, existingManifest, config.env);
|
|
321
371
|
|
|
322
372
|
const generated = [
|
|
323
373
|
{ path: join(appDirectory, SERVER_BUNDLE), text: server.text },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewx/app",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.20",
|
|
4
4
|
"description": "Task-scoped CLI access to CrewX Apps",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/src/engine.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"esbuild": "0.27.7",
|
|
31
|
-
"@crewx/sdk": "0.9.0-rc.
|
|
31
|
+
"@crewx/sdk": "0.9.0-rc.139"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
34
34
|
"crewx",
|