@crewx/app 0.1.0-rc.26 → 0.1.0-rc.28
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/build-app/index.mjs +7 -618
- package/build-app/load.cjs +1 -4
- package/build-app/manifest.mjs +1 -63
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -91
- package/dist/src/engine.d.ts +1 -1
- package/dist/src/engine.js +7 -1536
- package/dist/src/receipt-wait.d.ts +1 -1
- package/dist/src/receipt-wait.js +1 -41
- package/dist/src/usage.d.ts +1 -1
- package/dist/src/usage.js +8 -191
- package/package.json +2 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/src/engine.d.ts.map +0 -1
- package/dist/src/engine.js.map +0 -1
- package/dist/src/receipt-wait.d.ts.map +0 -1
- package/dist/src/receipt-wait.js.map +0 -1
- package/dist/src/usage.d.ts.map +0 -1
- package/dist/src/usage.js.map +0 -1
package/build-app/index.mjs
CHANGED
|
@@ -1,618 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// Output: <app>/dist/server.mjs the App bundle (also exports runSkillApp, generateManifest, ...)
|
|
9
|
-
//
|
|
10
|
-
// Bridge mode (package.json has a "bin" or crewx.app.command): the App is a CLI that the manifest's
|
|
11
|
-
// <app>/dist/entry.mjs the one-shot process entry (runAppEntry) for the skill CLI
|
|
12
|
-
// <app>/manifest.json the App manifest; only _meta["dev.crewx"].actions is regenerated
|
|
13
|
-
// from the @Tool methods, every other key stays as authored
|
|
14
|
-
//
|
|
15
|
-
// Tool mode (neither): server.ts is the whole backend. Nothing is written into the manifest; the build
|
|
16
|
-
// writes the declarations and a self-starting entry next to the bundle, and the Host reads them from there.
|
|
17
|
-
// <app>/dist/main.mjs the self-starting entry: runAppEntry over ./server.mjs, no argument
|
|
18
|
-
// <app>/dist/tools.json { "schemaVersion": 1, "actions": [...] }: one object per @Tool, no backend
|
|
19
|
-
// <app>/manifest.json only _meta["dev.crewx"].actions is removed; every other key stays as authored
|
|
20
|
-
//
|
|
21
|
-
// The build never writes ui-plugin.json. A new App (no manifest.json yet) gets a whole manifest.json
|
|
22
|
-
// derived from the static definition in server.ts, with release version 0.1.0.
|
|
23
|
-
//
|
|
24
|
-
// `@crewx/sdk/app` resolves to the SDK source (packages/sdk/src/app/index.ts) when the app lives in
|
|
25
|
-
// the CrewX repository this file is part of, so an SDK source edit reaches the next app build
|
|
26
|
-
// without `pnpm build:sdk`. Anywhere else it resolves to the SDK the CrewX CLI ships with (the
|
|
27
|
-
// @crewx/sdk next to @crewx/app). Either way the SDK code is inlined: no `@crewx/sdk` import is
|
|
28
|
-
// left in the bundles.
|
|
29
|
-
//
|
|
30
|
-
// Optional app settings in <app>/package.json:
|
|
31
|
-
// "crewx": { "app": {
|
|
32
|
-
// "command": ["sheet", "app"], // skill command the actions run; default: [<the one bin name>, "app"].
|
|
33
|
-
// // With neither a bin nor a command the App is in tool mode.
|
|
34
|
-
// "external": ["lib/**"], // app files (globs relative to the app) kept out of the bundle
|
|
35
|
-
// // and imported at run time from their real place
|
|
36
|
-
// "env": ["CREWX_APP_INSTANCE_ID", "CREWX_THREAD_ID"]
|
|
37
|
-
// // written to every generated action's backend.env, so the Host
|
|
38
|
-
// // puts these values in the process environment; absent = no env key.
|
|
39
|
-
// // Bridge mode only: a tool-mode process is always told both.
|
|
40
|
-
// } }
|
|
41
|
-
|
|
42
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, watch, writeFileSync } from 'node:fs';
|
|
43
|
-
import { createRequire } from 'node:module';
|
|
44
|
-
import { tmpdir } from 'node:os';
|
|
45
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
46
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
|
-
import { CREWX_META_NAMESPACE, convertV2ToManifest } from './manifest.mjs';
|
|
48
|
-
|
|
49
|
-
const ownDirectory = dirname(fileURLToPath(import.meta.url));
|
|
50
|
-
const requireFromPackage = createRequire(import.meta.url);
|
|
51
|
-
|
|
52
|
-
// In the CrewX repository this file is <repository>/packages/built-in/app/build-app/index.mjs.
|
|
53
|
-
// Installed from npm it sits under node_modules, where the same four steps up do not reach a
|
|
54
|
-
// repository, so the layout check below is what tells the two apart.
|
|
55
|
-
const REPOSITORY_LAYOUT = join('packages', 'built-in', 'app', 'build-app');
|
|
56
|
-
const repositoryRoot = resolve(ownDirectory, '..', '..', '..', '..');
|
|
57
|
-
export const SDK_APP_SOURCE_ENTRY = join(repositoryRoot, 'packages', 'sdk', 'src', 'app', 'index.ts');
|
|
58
|
-
const SDK_APP_SOURCE_DIRECTORY = dirname(SDK_APP_SOURCE_ENTRY);
|
|
59
|
-
|
|
60
|
-
const SERVER_ENTRY = 'server.ts';
|
|
61
|
-
const SERVER_BUNDLE = 'dist/server.mjs';
|
|
62
|
-
const ENTRY_BUNDLE = 'dist/entry.mjs';
|
|
63
|
-
// Tool mode. `dist/entry.mjs` is not reused: bridge launchers import it as a library (apps/timer/timer.mjs).
|
|
64
|
-
const MAIN_BUNDLE = 'dist/main.mjs';
|
|
65
|
-
const TOOLS_FILE = 'dist/tools.json';
|
|
66
|
-
const TOOLS_SCHEMA_VERSION = 1;
|
|
67
|
-
const MANIFEST_FILE = 'manifest.json';
|
|
68
|
-
const WATCH_DEBOUNCE_MS = 100;
|
|
69
|
-
|
|
70
|
-
// What dist/server.mjs exposes on top of the app's own exports. sheet.mjs-style launchers call
|
|
71
|
-
// runSkillApp through runAppEntry; the build itself calls generateManifest.
|
|
72
|
-
const SERVER_WRAPPER = [
|
|
73
|
-
`import App from './${SERVER_ENTRY}';`,
|
|
74
|
-
`export * from './${SERVER_ENTRY}';`,
|
|
75
|
-
'export default App;',
|
|
76
|
-
"export { AppError, CrewXApp, Tool, executeAppRequest, generateManifest, getToolDeclarations, runSkillApp } from '@crewx/sdk/app';",
|
|
77
|
-
'',
|
|
78
|
-
].join('\n');
|
|
79
|
-
const ENTRY_WRAPPER = "export { runAppEntry } from '@crewx/sdk/app';\n";
|
|
80
|
-
// The tool-mode process entry: server.mjs stays its own file, and the entry imports it at run time.
|
|
81
|
-
const MAIN_WRAPPER = [
|
|
82
|
-
"import { runAppEntry } from '@crewx/sdk/app';",
|
|
83
|
-
"await runAppEntry(() => import('./server.mjs'));",
|
|
84
|
-
'',
|
|
85
|
-
].join('\n');
|
|
86
|
-
// The declarations are read from a bundle of their own (like the env rule below), so server.mjs keeps
|
|
87
|
-
// its exports and an older shipped SDK that lacks generateToolDeclarations only fails a tool-mode build.
|
|
88
|
-
const DECLARATIONS_WRAPPER = [
|
|
89
|
-
`import App from './${SERVER_ENTRY}';`,
|
|
90
|
-
'export default App;',
|
|
91
|
-
"export { generateToolDeclarations } from '@crewx/sdk/app';",
|
|
92
|
-
'',
|
|
93
|
-
].join('\n');
|
|
94
|
-
// The rule the manifest reader applies to backend.env. It is bundled on its own and only imported by
|
|
95
|
-
// the build, so it never reaches dist/server.mjs (and every App without crewx.app.env builds the same bytes).
|
|
96
|
-
const ENV_RULE_WRAPPER = "export { SKILL_BACKEND_ENV_NAMES, isSkillBackendEnv } from '@crewx/sdk/app';\n";
|
|
97
|
-
const ENV_RULE_BUNDLE = 'dist/.env-rule.mjs'; // only names the esbuild output; the file is written under the OS temp folder
|
|
98
|
-
|
|
99
|
-
// The published @crewx/sdk/app is CommonJS. Inlined into an ES module, its require() calls
|
|
100
|
-
// (node built-ins) need a real require.
|
|
101
|
-
const COMMONJS_INTEROP_BANNER = "import { createRequire as __crewxCreateRequire } from 'node:module'; const require = __crewxCreateRequire(import.meta.url);";
|
|
102
|
-
|
|
103
|
-
const TSCONFIG_RAW = {
|
|
104
|
-
compilerOptions: {
|
|
105
|
-
target: 'ES2022',
|
|
106
|
-
module: 'ESNext',
|
|
107
|
-
moduleResolution: 'Bundler',
|
|
108
|
-
experimentalDecorators: false,
|
|
109
|
-
emitDecoratorMetadata: false,
|
|
110
|
-
useDefineForClassFields: true,
|
|
111
|
-
lib: ['ES2022', 'esnext.decorators'],
|
|
112
|
-
allowJs: true,
|
|
113
|
-
checkJs: false,
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The one place that knows where the @Tool declarations are written: the `actions` of
|
|
119
|
-
* `<app>/manifest.json`, i.e. `_meta["dev.crewx"].actions`. `existingText` is the manifest.json
|
|
120
|
-
* already on disk. With it only `actions` is replaced, so every other key keeps its value and its
|
|
121
|
-
* place (the file is written back as two-space JSON). Without it the whole file is derived from the
|
|
122
|
-
* App's static definition and starts at release version 0.1.0.
|
|
123
|
-
*
|
|
124
|
-
* `backendEnv` (crewx.app.env, already checked by the caller) is copied to the `backend.env` of every
|
|
125
|
-
* action; left out, no action gets an `env` key.
|
|
126
|
-
*/
|
|
127
|
-
export function renderActionDeclarations(imported, command, existingText, backendEnv) {
|
|
128
|
-
let generated = imported.generateManifest(imported.default, command);
|
|
129
|
-
if (backendEnv !== undefined && Array.isArray(generated.actions)) {
|
|
130
|
-
generated = {
|
|
131
|
-
...generated,
|
|
132
|
-
actions: generated.actions.map((action) => ({ ...action, backend: { ...action.backend, env: [...backendEnv] } })),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
let manifest;
|
|
136
|
-
if (existingText === undefined) {
|
|
137
|
-
manifest = convertV2ToManifest(generated);
|
|
138
|
-
} else {
|
|
139
|
-
try {
|
|
140
|
-
manifest = JSON.parse(existingText);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
throw new Error(`${MANIFEST_FILE} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
143
|
-
}
|
|
144
|
-
const dev = manifest?._meta?.[CREWX_META_NAMESPACE];
|
|
145
|
-
if (dev === null || typeof dev !== 'object' || Array.isArray(dev)) {
|
|
146
|
-
throw new Error(`${MANIFEST_FILE} must have an object at _meta["${CREWX_META_NAMESPACE}"]`);
|
|
147
|
-
}
|
|
148
|
-
dev.actions = generated.actions;
|
|
149
|
-
}
|
|
150
|
-
return { fileName: MANIFEST_FILE, text: `${JSON.stringify(manifest, null, 2)}\n` };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Tool mode counterpart of `renderActionDeclarations`: the declarations go to `dist/tools.json`, and the
|
|
155
|
-
* manifest loses `_meta["dev.crewx"].actions` (every other key keeps its value and place). A new App gets a
|
|
156
|
-
* whole manifest.json derived from the static definition, which has no `actions`. `imported` is the
|
|
157
|
-
* declarations bundle: its default export is the App class, and it exports `generateToolDeclarations`.
|
|
158
|
-
*/
|
|
159
|
-
export function renderToolDeclarations(imported, existingText) {
|
|
160
|
-
const actions = imported.generateToolDeclarations(imported.default);
|
|
161
|
-
let manifest;
|
|
162
|
-
if (existingText === undefined) {
|
|
163
|
-
manifest = convertV2ToManifest({ ...imported.default.definition });
|
|
164
|
-
} else {
|
|
165
|
-
try {
|
|
166
|
-
manifest = JSON.parse(existingText);
|
|
167
|
-
} catch (error) {
|
|
168
|
-
throw new Error(`${MANIFEST_FILE} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
169
|
-
}
|
|
170
|
-
const dev = manifest?._meta?.[CREWX_META_NAMESPACE];
|
|
171
|
-
if (dev === null || typeof dev !== 'object' || Array.isArray(dev)) {
|
|
172
|
-
throw new Error(`${MANIFEST_FILE} must have an object at _meta["${CREWX_META_NAMESPACE}"]`);
|
|
173
|
-
}
|
|
174
|
-
delete dev.actions;
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
fileName: MANIFEST_FILE,
|
|
178
|
-
text: `${JSON.stringify(manifest, null, 2)}\n`,
|
|
179
|
-
toolsFileName: TOOLS_FILE,
|
|
180
|
-
toolsText: `${JSON.stringify({ schemaVersion: TOOLS_SCHEMA_VERSION, actions }, null, 2)}\n`,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* `[<appDir>] [--app-dir=<dir>] [--check | --watch]`. The app folder is the current directory
|
|
186
|
-
* unless one is given, once, either way.
|
|
187
|
-
*/
|
|
188
|
-
export function parseArguments(argv, cwd = process.cwd()) {
|
|
189
|
-
let appDirectory;
|
|
190
|
-
let check = false;
|
|
191
|
-
let watchMode = false;
|
|
192
|
-
const setAppDirectory = (directory) => {
|
|
193
|
-
if (appDirectory !== undefined) throw new Error('Give the app folder only once');
|
|
194
|
-
appDirectory = resolve(cwd, directory);
|
|
195
|
-
};
|
|
196
|
-
for (const argument of argv) {
|
|
197
|
-
if (argument === '--check') check = true;
|
|
198
|
-
else if (argument === '--watch') watchMode = true;
|
|
199
|
-
else if (argument.startsWith('--app-dir=')) setAppDirectory(argument.slice('--app-dir='.length));
|
|
200
|
-
else if (argument.startsWith('-')) throw new Error(`Unknown argument: ${argument}`);
|
|
201
|
-
else setAppDirectory(argument);
|
|
202
|
-
}
|
|
203
|
-
if (check && watchMode) throw new Error('--check and --watch cannot be combined');
|
|
204
|
-
return { appDirectory: appDirectory ?? cwd, check, watch: watchMode };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function readAppConfig(appDirectory) {
|
|
208
|
-
const packagePath = join(appDirectory, 'package.json');
|
|
209
|
-
if (!existsSync(packagePath)) throw new Error(`${packagePath} not found; run the build from an app folder or give the app folder`);
|
|
210
|
-
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
211
|
-
const config = pkg.crewx?.app ?? {};
|
|
212
|
-
if (isToolMode(pkg)) {
|
|
213
|
-
if (config.env !== undefined) {
|
|
214
|
-
throw new Error('crewx.app.env is only for an App with a bin or crewx.app.command; a server.ts-only App always gets its instance and thread as this.context');
|
|
215
|
-
}
|
|
216
|
-
const toolExternal = config.external ?? [];
|
|
217
|
-
if (!Array.isArray(toolExternal) || !toolExternal.every((glob) => typeof glob === 'string')) {
|
|
218
|
-
throw new Error('crewx.app.external must be an array of globs');
|
|
219
|
-
}
|
|
220
|
-
// No `command` key: that is how the build tells a tool-mode App from a bridge App.
|
|
221
|
-
return { external: toolExternal };
|
|
222
|
-
}
|
|
223
|
-
const command = config.command ?? deriveCommand(pkg);
|
|
224
|
-
if (!Array.isArray(command) || command.length === 0 || !command.every((token) => typeof token === 'string' && token !== '')) {
|
|
225
|
-
throw new Error('crewx.app.command must be a non-empty array of strings');
|
|
226
|
-
}
|
|
227
|
-
const external = config.external ?? [];
|
|
228
|
-
if (!Array.isArray(external) || !external.every((glob) => typeof glob === 'string')) {
|
|
229
|
-
throw new Error('crewx.app.external must be an array of globs');
|
|
230
|
-
}
|
|
231
|
-
const env = config.env;
|
|
232
|
-
if (env !== undefined && (!Array.isArray(env) || !env.every((name) => typeof name === 'string'))) {
|
|
233
|
-
throw new Error('crewx.app.env must be an array of environment variable names');
|
|
234
|
-
}
|
|
235
|
-
return { command, external, ...(env === undefined ? {} : { env }) };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Tool mode: `server.ts` is the whole backend. A package.json with no `bin` and no `crewx.app.command` has
|
|
240
|
-
* no CLI for the manifest to call, so there is nothing to bridge. (`main` does not matter: an App may keep
|
|
241
|
-
* one for a command an agent runs by hand.)
|
|
242
|
-
*/
|
|
243
|
-
function isToolMode(pkg) {
|
|
244
|
-
return pkg.bin === undefined && pkg.crewx?.app?.command === undefined;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Refuses a `crewx.app.env` the manifest reader would refuse, by asking the reader's own rule. */
|
|
248
|
-
async function assertBackendEnv(esbuild, appDirectory, config) {
|
|
249
|
-
let ruleBundle;
|
|
250
|
-
try {
|
|
251
|
-
ruleBundle = await bundle(esbuild, appDirectory, config, {
|
|
252
|
-
contents: ENV_RULE_WRAPPER, sourcefile: 'crewx-app-env-rule.ts', outfile: ENV_RULE_BUNDLE,
|
|
253
|
-
});
|
|
254
|
-
} catch (error) {
|
|
255
|
-
if (error.errors?.some(({ text }) => /No matching export/u.test(text))) {
|
|
256
|
-
throw new Error('crewx.app.env needs a newer @crewx/sdk: reinstall the CrewX CLI');
|
|
257
|
-
}
|
|
258
|
-
throw error;
|
|
259
|
-
}
|
|
260
|
-
// The bundle imports nothing from the app, so it runs from the OS temp folder and leaves dist/ alone.
|
|
261
|
-
const temporaryDirectory = mkdtempSync(join(tmpdir(), 'crewx-app-env-'));
|
|
262
|
-
try {
|
|
263
|
-
const temporaryRule = join(temporaryDirectory, 'rule.mjs');
|
|
264
|
-
writeFileSync(temporaryRule, ruleBundle.text, 'utf8');
|
|
265
|
-
const rule = await import(pathToFileURL(temporaryRule).href);
|
|
266
|
-
if (!rule.isSkillBackendEnv(config.env)) {
|
|
267
|
-
throw new Error(`crewx.app.env must be a non-empty list of distinct names from ${rule.SKILL_BACKEND_ENV_NAMES.join(', ')}`);
|
|
268
|
-
}
|
|
269
|
-
} finally {
|
|
270
|
-
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function deriveCommand(pkg) {
|
|
275
|
-
const bin = pkg.bin;
|
|
276
|
-
const names = typeof bin === 'string'
|
|
277
|
-
? [String(pkg.name ?? '').replace(/^@[^/]+\//u, '')]
|
|
278
|
-
: Object.keys(bin ?? {});
|
|
279
|
-
if (names.length !== 1 || names[0] === '') {
|
|
280
|
-
throw new Error('cannot derive the skill command: give package.json exactly one "bin" name, or set crewx.app.command');
|
|
281
|
-
}
|
|
282
|
-
return [names[0], 'app'];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function globToRegExp(glob) {
|
|
286
|
-
let pattern = '^';
|
|
287
|
-
for (let index = 0; index < glob.length; index += 1) {
|
|
288
|
-
const character = glob[index];
|
|
289
|
-
if (character === '*' && glob[index + 1] === '*') {
|
|
290
|
-
pattern += '.*';
|
|
291
|
-
index += 1;
|
|
292
|
-
} else if (character === '*') {
|
|
293
|
-
pattern += '[^/]*';
|
|
294
|
-
} else if (character === '?') {
|
|
295
|
-
pattern += '[^/]';
|
|
296
|
-
} else {
|
|
297
|
-
pattern += character.replace(/[\\^$+{}()[\].|]/g, '\\$&');
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return new RegExp(`${pattern}$`);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function isInside(parent, child) {
|
|
304
|
-
const relativePath = relative(parent, child);
|
|
305
|
-
return relativePath !== '' && !relativePath.startsWith('..') && !isAbsolute(relativePath);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** True when the app folder is part of the CrewX repository this file belongs to, so the SDK source is the right target. */
|
|
309
|
-
export function usesSdkSource(appDirectory) {
|
|
310
|
-
if (ownDirectory !== join(repositoryRoot, REPOSITORY_LAYOUT)) return false;
|
|
311
|
-
if (!existsSync(SDK_APP_SOURCE_ENTRY)) return false;
|
|
312
|
-
return isInside(realpathSync(repositoryRoot), realpathSync(appDirectory));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function loadEsbuild(appDirectory) {
|
|
316
|
-
// Prefer the esbuild the app pins, so its output matches what its own build produced before.
|
|
317
|
-
try {
|
|
318
|
-
const resolved = createRequire(join(appDirectory, 'package.json')).resolve('esbuild');
|
|
319
|
-
return await import(pathToFileURL(resolved).href);
|
|
320
|
-
} catch {
|
|
321
|
-
// Otherwise the esbuild that comes with @crewx/app.
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
return await import('esbuild');
|
|
325
|
-
} catch {
|
|
326
|
-
throw new Error('cannot load esbuild: it comes with @crewx/app, so reinstall the CrewX CLI');
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// The shipped SDK is bundled under this virtual name instead of its real path. esbuild writes a
|
|
331
|
-
// CommonJS module's path into the bundle (`__commonJS({ "<path>"(exports) {`); the real path is
|
|
332
|
-
// <install>/node_modules/@crewx/sdk/dist/app/index.js, which would put "@crewx/sdk" into the bundle
|
|
333
|
-
// and make its bytes depend on where the CLI is installed.
|
|
334
|
-
const SHIPPED_SDK_NAMESPACE = 'crewx-sdk';
|
|
335
|
-
const SHIPPED_SDK_MODULE = 'app/index.js';
|
|
336
|
-
|
|
337
|
-
/** The `@crewx/sdk/app` file the CLI ships with (the @crewx/sdk next to @crewx/app), or undefined. */
|
|
338
|
-
function shippedSdkEntry() {
|
|
339
|
-
try {
|
|
340
|
-
return requireFromPackage.resolve('@crewx/sdk/app');
|
|
341
|
-
} catch {
|
|
342
|
-
return undefined;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function createPlugins(appDirectory, config) {
|
|
347
|
-
const externalPatterns = config.external.map(globToRegExp);
|
|
348
|
-
const sdkFromSource = usesSdkSource(appDirectory);
|
|
349
|
-
return [{
|
|
350
|
-
name: 'crewx-sdk-app',
|
|
351
|
-
setup(buildContext) {
|
|
352
|
-
buildContext.onResolve({ filter: /^@crewx\/sdk\/app$/ }, () => {
|
|
353
|
-
if (sdkFromSource) return { path: SDK_APP_SOURCE_ENTRY };
|
|
354
|
-
const shipped = shippedSdkEntry();
|
|
355
|
-
if (shipped === undefined) {
|
|
356
|
-
return { errors: [{ text: 'cannot resolve @crewx/sdk/app: it comes with the CrewX CLI, so reinstall the CLI' }] };
|
|
357
|
-
}
|
|
358
|
-
return { path: SHIPPED_SDK_MODULE, namespace: SHIPPED_SDK_NAMESPACE, pluginData: { file: shipped } };
|
|
359
|
-
});
|
|
360
|
-
buildContext.onLoad({ filter: /.*/, namespace: SHIPPED_SDK_NAMESPACE }, ({ pluginData }) => ({
|
|
361
|
-
contents: readFileSync(pluginData.file, 'utf8'),
|
|
362
|
-
loader: 'js',
|
|
363
|
-
resolveDir: dirname(pluginData.file),
|
|
364
|
-
}));
|
|
365
|
-
},
|
|
366
|
-
}, {
|
|
367
|
-
name: 'crewx-app-external-files',
|
|
368
|
-
setup(buildContext) {
|
|
369
|
-
if (externalPatterns.length === 0) return;
|
|
370
|
-
buildContext.onResolve({ filter: /^\.\.?\// }, ({ path, resolveDir }) => {
|
|
371
|
-
const absolute = resolve(resolveDir, path);
|
|
372
|
-
const fromApp = relative(appDirectory, absolute).split(sep).join('/');
|
|
373
|
-
if (fromApp.startsWith('..') || !externalPatterns.some((pattern) => pattern.test(fromApp))) return undefined;
|
|
374
|
-
const fromDist = relative(join(appDirectory, 'dist'), absolute).split(sep).join('/');
|
|
375
|
-
return { path: fromDist.startsWith('.') ? fromDist : `./${fromDist}`, external: true };
|
|
376
|
-
});
|
|
377
|
-
},
|
|
378
|
-
}];
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// The tool-mode entry imports ./server.mjs when it runs, from dist/ next to it: not a file to bundle.
|
|
382
|
-
const SERVER_IMPORT_PLUGIN = {
|
|
383
|
-
name: 'crewx-app-server-import',
|
|
384
|
-
setup(buildContext) {
|
|
385
|
-
buildContext.onResolve({ filter: /^\.\/server\.mjs$/ }, ({ path }) => ({ path, external: true }));
|
|
386
|
-
},
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
async function bundle(esbuild, appDirectory, config, { contents, sourcefile, outfile, metafile = false, plugins = [] }) {
|
|
390
|
-
const result = await esbuild.build({
|
|
391
|
-
...(usesSdkSource(appDirectory) ? {} : { banner: { js: COMMONJS_INTEROP_BANNER } }),
|
|
392
|
-
absWorkingDir: appDirectory,
|
|
393
|
-
stdin: { contents, resolveDir: appDirectory, sourcefile, loader: 'ts' },
|
|
394
|
-
outfile,
|
|
395
|
-
bundle: true,
|
|
396
|
-
write: false,
|
|
397
|
-
metafile,
|
|
398
|
-
format: 'esm',
|
|
399
|
-
platform: 'node',
|
|
400
|
-
target: 'node22',
|
|
401
|
-
packages: 'external',
|
|
402
|
-
plugins: [...createPlugins(appDirectory, config), ...plugins],
|
|
403
|
-
tsconfigRaw: TSCONFIG_RAW,
|
|
404
|
-
logLevel: 'silent',
|
|
405
|
-
}).catch((error) => {
|
|
406
|
-
// The wrapper imports `default` from server.ts; say what is wrong in the app's terms.
|
|
407
|
-
if (error.errors?.some(({ text }) => /No matching export in ".*server\.ts" for import "default"/u.test(text))) {
|
|
408
|
-
throw new Error(`${SERVER_ENTRY} must default-export a CrewXApp subclass with a static definition`);
|
|
409
|
-
}
|
|
410
|
-
throw error;
|
|
411
|
-
});
|
|
412
|
-
const expectedPath = join(appDirectory, outfile);
|
|
413
|
-
const text = result.outputFiles.find((file) => file.path === expectedPath)?.text;
|
|
414
|
-
if (text === undefined) throw new Error(`esbuild did not produce ${outfile}`);
|
|
415
|
-
if (/@crewx\/sdk(?:['"/])/u.test(text.replace(/^\s*\/\/.*$/gmu, ''))) {
|
|
416
|
-
throw new Error(`${outfile} still imports @crewx/sdk; server.ts may import only @crewx/sdk/app`);
|
|
417
|
-
}
|
|
418
|
-
return { text, inputs: result.metafile ? Object.keys(result.metafile.inputs) : [] };
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Builds the bundles and the action declarations. With `check` nothing is written and the
|
|
423
|
-
* result lists every generated file that differs from what is on disk.
|
|
424
|
-
*
|
|
425
|
-
* Returns `stale` (paths relative to the current directory, empty unless `check`), `staleFiles`
|
|
426
|
-
* (the same files relative to the app folder), `files` (every generated file, relative to the app
|
|
427
|
-
* folder), `actions` (the actionId of every @Tool), `sdkEntry` (the file `@crewx/sdk/app` resolved
|
|
428
|
-
* to: the SDK source in the repository, the shipped SDK anywhere else) and `inputs` (the source
|
|
429
|
-
* files the bundle read; the shipped SDK is not among them).
|
|
430
|
-
*/
|
|
431
|
-
export async function buildApp({ appDirectory, check = false, log = () => {} }) {
|
|
432
|
-
if (!existsSync(join(appDirectory, SERVER_ENTRY))) throw new Error(`${join(appDirectory, SERVER_ENTRY)} not found`);
|
|
433
|
-
const config = readAppConfig(appDirectory);
|
|
434
|
-
const toolMode = config.command === undefined;
|
|
435
|
-
const esbuild = await loadEsbuild(appDirectory);
|
|
436
|
-
if (config.env !== undefined) await assertBackendEnv(esbuild, appDirectory, config);
|
|
437
|
-
const distDirectory = join(appDirectory, 'dist');
|
|
438
|
-
const distDirectoryExisted = existsSync(distDirectory);
|
|
439
|
-
const temporaryBundle = join(distDirectory, `.server-build-${process.pid}.mjs`);
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
const server = await bundle(esbuild, appDirectory, config, {
|
|
443
|
-
contents: SERVER_WRAPPER, sourcefile: 'crewx-app-server.ts', outfile: SERVER_BUNDLE, metafile: true,
|
|
444
|
-
});
|
|
445
|
-
const entry = toolMode
|
|
446
|
-
? await bundle(esbuild, appDirectory, config, {
|
|
447
|
-
contents: MAIN_WRAPPER, sourcefile: 'crewx-app-main.ts', outfile: MAIN_BUNDLE, plugins: [SERVER_IMPORT_PLUGIN],
|
|
448
|
-
})
|
|
449
|
-
: await bundle(esbuild, appDirectory, config, {
|
|
450
|
-
contents: ENTRY_WRAPPER, sourcefile: 'crewx-app-entry.ts', outfile: ENTRY_BUNDLE,
|
|
451
|
-
});
|
|
452
|
-
// Tool mode reads the declarations from a bundle of their own; bridge mode reads them from server.mjs.
|
|
453
|
-
const evaluated = toolMode ? await bundleDeclarations(esbuild, appDirectory, config) : server;
|
|
454
|
-
|
|
455
|
-
// The bundle imports its externals relative to dist/, so it has to run from there.
|
|
456
|
-
mkdirSync(distDirectory, { recursive: true });
|
|
457
|
-
writeFileSync(temporaryBundle, evaluated.text, 'utf8');
|
|
458
|
-
const imported = await import(`${pathToFileURL(temporaryBundle).href}?build=${Date.now()}`);
|
|
459
|
-
if (typeof imported.default !== 'function' || imported.default.definition === undefined) {
|
|
460
|
-
throw new Error(`${SERVER_ENTRY} must default-export a CrewXApp subclass with a static definition`);
|
|
461
|
-
}
|
|
462
|
-
const manifestPath = join(appDirectory, MANIFEST_FILE);
|
|
463
|
-
const existingManifest = existsSync(manifestPath) ? readFileSync(manifestPath, 'utf8') : undefined;
|
|
464
|
-
const declarations = toolMode
|
|
465
|
-
? renderToolDeclarations(imported, existingManifest)
|
|
466
|
-
: renderActionDeclarations(imported, config.command, existingManifest, config.env);
|
|
467
|
-
|
|
468
|
-
const generated = [
|
|
469
|
-
{ path: join(appDirectory, SERVER_BUNDLE), text: server.text },
|
|
470
|
-
{ path: join(appDirectory, toolMode ? MAIN_BUNDLE : ENTRY_BUNDLE), text: entry.text },
|
|
471
|
-
...(toolMode ? [{ path: join(appDirectory, declarations.toolsFileName), text: declarations.toolsText }] : []),
|
|
472
|
-
{ path: join(appDirectory, declarations.fileName), text: declarations.text },
|
|
473
|
-
];
|
|
474
|
-
const staleEntries = generated.filter(({ path, text }) => !existsSync(path) || readFileSync(path, 'utf8') !== text);
|
|
475
|
-
const stale = staleEntries.map(({ path }) => relative(process.cwd(), path));
|
|
476
|
-
|
|
477
|
-
if (!check) {
|
|
478
|
-
for (const { path, text } of generated) writeFileSync(path, text, 'utf8');
|
|
479
|
-
const shown = (name) => relative(process.cwd(), join(appDirectory, name));
|
|
480
|
-
log(toolMode
|
|
481
|
-
? `Built ${shown(SERVER_BUNDLE)} and ${shown(MAIN_BUNDLE)}, wrote ${shown(TOOLS_FILE)} and regenerated ${shown(declarations.fileName)}.`
|
|
482
|
-
: `Built ${relative(process.cwd(), join(appDirectory, SERVER_BUNDLE))} and regenerated ${relative(process.cwd(), join(appDirectory, declarations.fileName))}.`);
|
|
483
|
-
}
|
|
484
|
-
return {
|
|
485
|
-
stale: check ? stale : [],
|
|
486
|
-
staleFiles: check ? staleEntries.map(({ path }) => relative(appDirectory, path)) : [],
|
|
487
|
-
files: generated.map(({ path }) => relative(appDirectory, path)),
|
|
488
|
-
actions: toolMode ? declaredToolActionIds(declarations.toolsText) : declaredActionIds(declarations.text),
|
|
489
|
-
mode: toolMode ? 'tool' : 'bridge',
|
|
490
|
-
sdkEntry: usesSdkSource(appDirectory) ? SDK_APP_SOURCE_ENTRY : shippedSdkEntry(),
|
|
491
|
-
inputs: server.inputs
|
|
492
|
-
.filter((input) => !input.startsWith(`${SHIPPED_SDK_NAMESPACE}:`))
|
|
493
|
-
.map((input) => resolve(appDirectory, input)),
|
|
494
|
-
};
|
|
495
|
-
} finally {
|
|
496
|
-
rmSync(temporaryBundle, { force: true });
|
|
497
|
-
if (!distDirectoryExisted && check) rmSync(distDirectory, { recursive: true, force: true });
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/** The bundle a tool-mode build evaluates: the App class and `generateToolDeclarations`, not written anywhere. */
|
|
502
|
-
async function bundleDeclarations(esbuild, appDirectory, config) {
|
|
503
|
-
try {
|
|
504
|
-
return await bundle(esbuild, appDirectory, config, {
|
|
505
|
-
contents: DECLARATIONS_WRAPPER, sourcefile: 'crewx-app-declarations.ts', outfile: 'dist/.declarations-build.mjs',
|
|
506
|
-
});
|
|
507
|
-
} catch (error) {
|
|
508
|
-
if (error.errors?.some(({ text }) => /No matching export/u.test(text) && /generateToolDeclarations/u.test(text))) {
|
|
509
|
-
throw new Error('a server.ts-only App (no bin, no crewx.app.command) needs a newer @crewx/sdk: reinstall the CrewX CLI');
|
|
510
|
-
}
|
|
511
|
-
throw error;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function declaredToolActionIds(toolsText) {
|
|
516
|
-
const actions = JSON.parse(toolsText)?.actions;
|
|
517
|
-
return Array.isArray(actions) ? actions.map((action) => action?.actionId).filter((id) => typeof id === 'string') : [];
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function declaredActionIds(manifestText) {
|
|
521
|
-
const actions = JSON.parse(manifestText)?._meta?.[CREWX_META_NAMESPACE]?.actions;
|
|
522
|
-
return Array.isArray(actions) ? actions.map((action) => action?.actionId).filter((id) => typeof id === 'string') : [];
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Rebuilds when an imported source file changes; SDK source edits count when the app uses it.
|
|
527
|
-
* Runs until `signal` aborts (never, without one) and then closes its watchers.
|
|
528
|
-
*/
|
|
529
|
-
export async function watchApp(appDirectory, log, { signal } = {}) {
|
|
530
|
-
const watchers = new Map();
|
|
531
|
-
let building = false;
|
|
532
|
-
let queued = false;
|
|
533
|
-
let timer;
|
|
534
|
-
let inputs = new Set();
|
|
535
|
-
|
|
536
|
-
const rebuild = async () => {
|
|
537
|
-
if (building) {
|
|
538
|
-
queued = true;
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
building = true;
|
|
542
|
-
try {
|
|
543
|
-
const result = await buildApp({ appDirectory, log });
|
|
544
|
-
inputs = new Set(result.inputs);
|
|
545
|
-
} catch (error) {
|
|
546
|
-
log(`Build failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
547
|
-
} finally {
|
|
548
|
-
building = false;
|
|
549
|
-
if (!signal?.aborted) refreshWatchers();
|
|
550
|
-
if (queued && !signal?.aborted) {
|
|
551
|
-
queued = false;
|
|
552
|
-
void rebuild();
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
const refreshWatchers = () => {
|
|
558
|
-
const directories = new Set([appDirectory, ...[...inputs].map((input) => dirname(input))]);
|
|
559
|
-
if (usesSdkSource(appDirectory)) directories.add(SDK_APP_SOURCE_DIRECTORY);
|
|
560
|
-
for (const [directory, watcher] of watchers) {
|
|
561
|
-
if (!directories.has(directory)) {
|
|
562
|
-
watcher.close();
|
|
563
|
-
watchers.delete(directory);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
for (const directory of directories) {
|
|
567
|
-
if (watchers.has(directory) || !existsSync(directory)) continue;
|
|
568
|
-
watchers.set(directory, watch(directory, (_event, filename) => {
|
|
569
|
-
if (filename === null) return;
|
|
570
|
-
const changed = join(directory, filename.toString());
|
|
571
|
-
// Generated files live next to the sources; reacting to them would loop.
|
|
572
|
-
const relevant = inputs.has(changed)
|
|
573
|
-
|| changed === join(appDirectory, 'package.json')
|
|
574
|
-
|| (directory === SDK_APP_SOURCE_DIRECTORY && /\.tsx?$/u.test(changed));
|
|
575
|
-
if (!relevant) return;
|
|
576
|
-
clearTimeout(timer);
|
|
577
|
-
timer = setTimeout(() => void rebuild(), WATCH_DEBOUNCE_MS);
|
|
578
|
-
}));
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
await rebuild();
|
|
583
|
-
log('Watching for changes. Press Ctrl+C to stop.');
|
|
584
|
-
await new Promise((resolveStopped) => {
|
|
585
|
-
if (signal === undefined) return;
|
|
586
|
-
if (signal.aborted) resolveStopped();
|
|
587
|
-
else signal.addEventListener('abort', resolveStopped, { once: true });
|
|
588
|
-
});
|
|
589
|
-
clearTimeout(timer);
|
|
590
|
-
for (const watcher of watchers.values()) watcher.close();
|
|
591
|
-
watchers.clear();
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* The command line of scripts/build-app.mjs: plain-text lines on stdout, errors on stderr.
|
|
596
|
-
* Returns the exit code (0 ok, 1 failed or stale).
|
|
597
|
-
*/
|
|
598
|
-
export async function runScript(argv, { cwd = process.cwd(), stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
599
|
-
const log = (message) => stdout.write(`${message}\n`);
|
|
600
|
-
try {
|
|
601
|
-
const options = parseArguments(argv, cwd);
|
|
602
|
-
if (options.watch) {
|
|
603
|
-
await watchApp(options.appDirectory, log);
|
|
604
|
-
return 0;
|
|
605
|
-
}
|
|
606
|
-
const { stale } = await buildApp({ appDirectory: options.appDirectory, check: options.check, log });
|
|
607
|
-
if (!options.check) return 0;
|
|
608
|
-
if (stale.length > 0) {
|
|
609
|
-
stderr.write(`Stale generated files: ${stale.join(', ')}\n`);
|
|
610
|
-
return 1;
|
|
611
|
-
}
|
|
612
|
-
log(`${basename(options.appDirectory)} App build artifacts are in sync.`);
|
|
613
|
-
return 0;
|
|
614
|
-
} catch (error) {
|
|
615
|
-
stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
616
|
-
return 1;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
1
|
+
import{existsSync as x,mkdirSync as z,mkdtempSync as Q,readFileSync as R,realpathSync as W,rmSync as C,watch as Z,writeFileSync as O}from"node:fs";import{createRequire as F}from"node:module";import{tmpdir as ee}from"node:os";import{basename as te,dirname as _,isAbsolute as re,join as c,relative as h,resolve as N,sep as U}from"node:path";import{fileURLToPath as ne,pathToFileURL as T}from"node:url";import{CREWX_META_NAMESPACE as S,convertV2ToManifest as J}from"./manifest.mjs";const D=_(ne(import.meta.url));const oe=F(import.meta.url);const se=c("packages","built-in","app","build-app");const L=N(D,"..","..","..","..");const $=c(L,"packages","sdk","src","app","index.ts");const V=_($);const E="server.ts";const k="dist/server.mjs";const q="dist/entry.mjs";const I="dist/main.mjs";const K="dist/tools.json";const ae=1;const y="manifest.json";const ie=100;const ce=[`import App from './${E}';`,`export * from './${E}';`,"export default App;","export { AppError, CrewXApp, Tool, executeAppRequest, generateManifest, getToolDeclarations, runSkillApp } from '@crewx/sdk/app';",""].join("\n");const le="export { runAppEntry } from '@crewx/sdk/app';\n";const fe=["import { runAppEntry } from '@crewx/sdk/app';","await runAppEntry(() => import('./server.mjs'));",""].join("\n");const ue=[`import App from './${E}';`,"export default App;","export { generateToolDeclarations } from '@crewx/sdk/app';",""].join("\n");const de="export { SKILL_BACKEND_ENV_NAMES, isSkillBackendEnv } from '@crewx/sdk/app';\n";const pe="dist/.env-rule.mjs";const me="import { createRequire as __crewxCreateRequire } from 'node:module'; const require = __crewxCreateRequire(import.meta.url);";const we={compilerOptions:{target:"ES2022",module:"ESNext",moduleResolution:"Bundler",experimentalDecorators:false,emitDecoratorMetadata:false,useDefineForClassFields:true,lib:["ES2022","esnext.decorators"],allowJs:true,checkJs:false}};function he(e,r,n,o){let s=e.generateManifest(e.default,r);if(o!==void 0&&Array.isArray(s.actions)){s={...s,actions:s.actions.map(a=>({...a,backend:{...a.backend,env:[...o]}}))}}let t;if(n===void 0){t=J(s)}else{try{t=JSON.parse(n)}catch(i){throw new Error(`${y} is not valid JSON: ${i instanceof Error?i.message:String(i)}`)}const a=t?._meta?.[S];if(a===null||typeof a!=="object"||Array.isArray(a)){throw new Error(`${y} must have an object at _meta["${S}"]`)}a.actions=s.actions}return{fileName:y,text:`${JSON.stringify(t,null,2)}
|
|
2
|
+
`}}function xe(e,r){const n=e.generateToolDeclarations(e.default);let o;if(r===void 0){o=J({...e.default.definition})}else{try{o=JSON.parse(r)}catch(t){throw new Error(`${y} is not valid JSON: ${t instanceof Error?t.message:String(t)}`)}const s=o?._meta?.[S];if(s===null||typeof s!=="object"||Array.isArray(s)){throw new Error(`${y} must have an object at _meta["${S}"]`)}delete s.actions}return{fileName:y,text:`${JSON.stringify(o,null,2)}
|
|
3
|
+
`,toolsFileName:K,toolsText:`${JSON.stringify({schemaVersion:ae,actions:n},null,2)}
|
|
4
|
+
`}}function Ee(e,r=process.cwd()){let n;let o=false;let s=false;const t=a=>{if(n!==void 0)throw new Error("Give the app folder only once");n=N(r,a)};for(const a of e){if(a==="--check")o=true;else if(a==="--watch")s=true;else if(a.startsWith("--app-dir="))t(a.slice("--app-dir=".length));else if(a.startsWith("-"))throw new Error(`Unknown argument: ${a}`);else t(a)}if(o&&s)throw new Error("--check and --watch cannot be combined");return{appDirectory:n??r,check:o,watch:s}}function ye(e){const r=c(e,"package.json");if(!x(r))throw new Error(`${r} not found; run the build from an app folder or give the app folder`);const n=JSON.parse(R(r,"utf8"));const o=n.crewx?.app??{};if(Ae(n)){if(o.env!==void 0){throw new Error("crewx.app.env is only for an App with a bin or crewx.app.command; a server.ts-only App always gets its instance and thread as this.context")}const i=o.external??[];if(!Array.isArray(i)||!i.every(d=>typeof d==="string")){throw new Error("crewx.app.external must be an array of globs")}return{external:i}}const s=o.command??ve(n);if(!Array.isArray(s)||s.length===0||!s.every(i=>typeof i==="string"&&i!=="")){throw new Error("crewx.app.command must be a non-empty array of strings")}const t=o.external??[];if(!Array.isArray(t)||!t.every(i=>typeof i==="string")){throw new Error("crewx.app.external must be an array of globs")}const a=o.env;if(a!==void 0&&(!Array.isArray(a)||!a.every(i=>typeof i==="string"))){throw new Error("crewx.app.env must be an array of environment variable names")}return{command:s,external:t,...a===void 0?{}:{env:a}}}function Ae(e){return e.bin===void 0&&e.crewx?.app?.command===void 0}async function ge(e,r,n){let o;try{o=await b(e,r,n,{contents:de,sourcefile:"crewx-app-env-rule.ts",outfile:pe})}catch(t){if(t.errors?.some(({text:a})=>/No matching export/u.test(a))){throw new Error("crewx.app.env needs a newer @crewx/sdk: reinstall the CrewX CLI")}throw t}const s=Q(c(ee(),"crewx-app-env-"));try{const t=c(s,"rule.mjs");O(t,o.text,"utf8");const a=await import(T(t).href);if(!a.isSkillBackendEnv(n.env)){throw new Error(`crewx.app.env must be a non-empty list of distinct names from ${a.SKILL_BACKEND_ENV_NAMES.join(", ")}`)}}finally{C(s,{recursive:true,force:true})}}function ve(e){const r=e.bin;const n=typeof r==="string"?[String(e.name??"").replace(/^@[^/]+\//u,"")]:Object.keys(r??{});if(n.length!==1||n[0]===""){throw new Error('cannot derive the skill command: give package.json exactly one "bin" name, or set crewx.app.command')}return[n[0],"app"]}function Se(e){let r="^";for(let n=0;n<e.length;n+=1){const o=e[n];if(o==="*"&&e[n+1]==="*"){r+=".*";n+=1}else if(o==="*"){r+="[^/]*"}else if(o==="?"){r+="[^/]"}else{r+=o.replace(/[\\^$+{}()[\].|]/g,"\\$&")}}return new RegExp(`${r}$`)}function be(e,r){const n=h(e,r);return n!==""&&!n.startsWith("..")&&!re(n)}function P(e){if(D!==c(L,se))return false;if(!x($))return false;return be(W(L),W(e))}async function Re(e){try{const r=F(c(e,"package.json")).resolve("esbuild");return await import(T(r).href)}catch{}try{return await import("esbuild")}catch{throw new Error("cannot load esbuild: it comes with @crewx/app, so reinstall the CrewX CLI")}}const M="crewx-sdk";const _e="app/index.js";function X(){try{return oe.resolve("@crewx/sdk/app")}catch{return void 0}}function Ne(e,r){const n=r.external.map(Se);const o=P(e);return[{name:"crewx-sdk-app",setup(s){s.onResolve({filter:/^@crewx\/sdk\/app$/},()=>{if(o)return{path:$};const t=X();if(t===void 0){return{errors:[{text:"cannot resolve @crewx/sdk/app: it comes with the CrewX CLI, so reinstall the CLI"}]}}return{path:_e,namespace:M,pluginData:{file:t}}});s.onLoad({filter:/.*/,namespace:M},({pluginData:t})=>({contents:R(t.file,"utf8"),loader:"js",resolveDir:_(t.file)}))}},{name:"crewx-app-external-files",setup(s){if(n.length===0)return;s.onResolve({filter:/^\.\.?\//},({path:t,resolveDir:a})=>{const i=N(a,t);const d=h(e,i).split(U).join("/");if(d.startsWith("..")||!n.some(l=>l.test(d)))return void 0;const p=h(c(e,"dist"),i).split(U).join("/");return{path:p.startsWith(".")?p:`./${p}`,external:true}})}}]}const $e={name:"crewx-app-server-import",setup(e){e.onResolve({filter:/^\.\/server\.mjs$/},({path:r})=>({path:r,external:true}))}};async function b(e,r,n,{contents:o,sourcefile:s,outfile:t,metafile:a=false,plugins:i=[]}){const d=await e.build({...P(r)?{}:{banner:{js:me}},absWorkingDir:r,stdin:{contents:o,resolveDir:r,sourcefile:s,loader:"ts"},outfile:t,bundle:true,write:false,metafile:a,format:"esm",platform:"node",target:"node22",packages:"external",plugins:[...Ne(r,n),...i],tsconfigRaw:we,logLevel:"silent"}).catch(f=>{if(f.errors?.some(({text:w})=>/No matching export in ".*server\.ts" for import "default"/u.test(w))){throw new Error(`${E} must default-export a CrewXApp subclass with a static definition`)}throw f});const p=c(r,t);const l=d.outputFiles.find(f=>f.path===p)?.text;if(l===void 0)throw new Error(`esbuild did not produce ${t}`);if(/@crewx\/sdk(?:['"/])/u.test(l.replace(/^\s*\/\/.*$/gmu,""))){throw new Error(`${t} still imports @crewx/sdk; server.ts may import only @crewx/sdk/app`)}return{text:l,inputs:d.metafile?Object.keys(d.metafile.inputs):[]}}async function Y({appDirectory:e,check:r=false,log:n=()=>{}}){if(!x(c(e,E)))throw new Error(`${c(e,E)} not found`);const o=ye(e);const s=o.command===void 0;const t=await Re(e);if(o.env!==void 0)await ge(t,e,o);const a=c(e,"dist");const i=x(a);const d=c(a,`.server-build-${process.pid}.mjs`);try{const p=await b(t,e,o,{contents:ce,sourcefile:"crewx-app-server.ts",outfile:k,metafile:true});const l=s?await b(t,e,o,{contents:fe,sourcefile:"crewx-app-main.ts",outfile:I,plugins:[$e]}):await b(t,e,o,{contents:le,sourcefile:"crewx-app-entry.ts",outfile:q});const f=s?await ke(t,e,o):p;z(a,{recursive:true});O(d,f.text,"utf8");const w=await import(`${T(d).href}?build=${Date.now()}`);if(typeof w.default!=="function"||w.default.definition===void 0){throw new Error(`${E} must default-export a CrewXApp subclass with a static definition`)}const g=c(e,y);const A=x(g)?R(g,"utf8"):void 0;const m=s?xe(w,A):he(w,o.command,A,o.env);const j=[{path:c(e,k),text:p.text},{path:c(e,s?I:q),text:l.text},...s?[{path:c(e,m.toolsFileName),text:m.toolsText}]:[],{path:c(e,m.fileName),text:m.text}];const B=j.filter(({path:u,text:v})=>!x(u)||R(u,"utf8")!==v);const H=B.map(({path:u})=>h(process.cwd(),u));if(!r){for(const{path:v,text:G}of j)O(v,G,"utf8");const u=v=>h(process.cwd(),c(e,v));n(s?`Built ${u(k)} and ${u(I)}, wrote ${u(K)} and regenerated ${u(m.fileName)}.`:`Built ${h(process.cwd(),c(e,k))} and regenerated ${h(process.cwd(),c(e,m.fileName))}.`)}return{stale:r?H:[],staleFiles:r?B.map(({path:u})=>h(e,u)):[],files:j.map(({path:u})=>h(e,u)),actions:s?Pe(m.toolsText):je(m.text),mode:s?"tool":"bridge",sdkEntry:P(e)?$:X(),inputs:p.inputs.filter(u=>!u.startsWith(`${M}:`)).map(u=>N(e,u))}}finally{C(d,{force:true});if(!i&&r)C(a,{recursive:true,force:true})}}async function ke(e,r,n){try{return await b(e,r,n,{contents:ue,sourcefile:"crewx-app-declarations.ts",outfile:"dist/.declarations-build.mjs"})}catch(o){if(o.errors?.some(({text:s})=>/No matching export/u.test(s)&&/generateToolDeclarations/u.test(s))){throw new Error("a server.ts-only App (no bin, no crewx.app.command) needs a newer @crewx/sdk: reinstall the CrewX CLI")}throw o}}function Pe(e){const r=JSON.parse(e)?.actions;return Array.isArray(r)?r.map(n=>n?.actionId).filter(n=>typeof n==="string"):[]}function je(e){const r=JSON.parse(e)?._meta?.[S]?.actions;return Array.isArray(r)?r.map(n=>n?.actionId).filter(n=>typeof n==="string"):[]}async function Ce(e,r,{signal:n}={}){const o=new Map;let s=false;let t=false;let a;let i=new Set;const d=async()=>{if(s){t=true;return}s=true;try{const l=await Y({appDirectory:e,log:r});i=new Set(l.inputs)}catch(l){r(`Build failed: ${l instanceof Error?l.message:String(l)}`)}finally{s=false;if(!n?.aborted)p();if(t&&!n?.aborted){t=false;void d()}}};const p=()=>{const l=new Set([e,...[...i].map(f=>_(f))]);if(P(e))l.add(V);for(const[f,w]of o){if(!l.has(f)){w.close();o.delete(f)}}for(const f of l){if(o.has(f)||!x(f))continue;o.set(f,Z(f,(w,g)=>{if(g===null)return;const A=c(f,g.toString());const m=i.has(A)||A===c(e,"package.json")||f===V&&/\.tsx?$/u.test(A);if(!m)return;clearTimeout(a);a=setTimeout(()=>void d(),ie)}))}};await d();r("Watching for changes. Press Ctrl+C to stop.");await new Promise(l=>{if(n===void 0)return;if(n.aborted)l();else n.addEventListener("abort",l,{once:true})});clearTimeout(a);for(const l of o.values())l.close();o.clear()}async function We(e,{cwd:r=process.cwd(),stdout:n=process.stdout,stderr:o=process.stderr}={}){const s=t=>n.write(`${t}
|
|
5
|
+
`);try{const t=Ee(e,r);if(t.watch){await Ce(t.appDirectory,s);return 0}const{stale:a}=await Y({appDirectory:t.appDirectory,check:t.check,log:s});if(!t.check)return 0;if(a.length>0){o.write(`Stale generated files: ${a.join(", ")}
|
|
6
|
+
`);return 1}s(`${te(t.appDirectory)} App build artifacts are in sync.`);return 0}catch(t){o.write(`${t instanceof Error?t.message:String(t)}
|
|
7
|
+
`);return 1}}export{$ as SDK_APP_SOURCE_ENTRY,Y as buildApp,Ee as parseArguments,ye as readAppConfig,he as renderActionDeclarations,xe as renderToolDeclarations,We as runScript,P as usesSdkSource,Ce as watchApp};
|
package/build-app/load.cjs
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// `import()` into `require()`. This file is plain CommonJS, so the `import()` below stays a real
|
|
3
|
-
// dynamic import (and works inside vitest, where `new Function('return import(...)')` does not).
|
|
4
|
-
module.exports = { loadAppBuilder: () => import('./index.mjs') };
|
|
1
|
+
var l=Object.create;var i=Object.defineProperty;var t=Object.getOwnPropertyDescriptor;var a=Object.getOwnPropertyNames;var m=Object.getPrototypeOf;var s=Object.prototype.hasOwnProperty;var u=(p,o,r,e)=>{if(o&&typeof o==="object"||typeof o==="function"){for(let d of a(o))if(!s.call(p,d)&&d!==r)i(p,d,{get:()=>o[d],enumerable:!(e=t(o,d))||e.enumerable})}return p};var x=(p,o,r)=>(r=p!=null?l(m(p)):{},u(o||!p||!p.__esModule?i(r,"default",{value:p,enumerable:true}):r,p));module.exports={loadAppBuilder:()=>import("./index.mjs")};
|