@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 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
@@ -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;
@@ -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/**"] // app files (globs relative to the app) kept out of the bundle
26
- // } } // and imported at run time from their real place
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
- const generated = imported.generateManifest(imported.default, command);
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
- return { command, external };
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.19",
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.138"
31
+ "@crewx/sdk": "0.9.0-rc.139"
32
32
  },
33
33
  "keywords": [
34
34
  "crewx",