Add development tooling: CLI scaffolding, server command, environment diagnostics, and template generators#437
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Add development helper script (scripts/dev.sh) with commands for setup, dev, build, test, clean, doctor, create, link - Add enhanced CLI commands: dev, doctor, create (plugin/example templates) - Add template generator script (scripts/generate.sh) for schemas and tests - Add comprehensive DEVELOPMENT.md guide (bilingual English/Chinese) - Add convenience npm scripts (doctor, setup, test) - Update README.md with development guide reference - Include script documentation in scripts/README.md Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
不需要 scripts 只需要 cli @copilot |
Remove scripts/ directory and update documentation to use CLI commands only.
All functionality previously in bash scripts is available through CLI commands:
- pnpm objectstack dev [package]
- pnpm objectstack doctor
- pnpm objectstack create {plugin|example}
- pnpm setup/doctor/test/build/clean (npm shortcuts)
Updated DEVELOPMENT.md, README.md, and TOOLCHAIN_DEMO.md to remove script references.
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
只需要英文文档,然后还需要启动服务的命令,启动服务的时候应该支持从配置文件中指定加载哪些插件,@copilot |
- Add `objectstack serve` command to start server with plugin configuration - Support loading plugins from objectstack.config.ts - Allow custom port via --port flag - Support headless mode with --no-server flag - Remove Chinese documentation, keep English only - Update DEVELOPMENT.md with serve command documentation - Update TOOLCHAIN_DEMO.md to English only Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
已完成:
使用方法: # 启动服务器(默认端口3000)
pnpm objectstack serve
# 指定端口
pnpm objectstack serve --port 8080配置示例: export default defineStack({
plugins: [
new HonoServerPlugin({ port: 3000 }),
// 其他插件...
],
});提交: 58b6dba |
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
| console.log(chalk.dim(`$ ${command}`)); | ||
| console.log(''); | ||
|
|
||
| execSync(command, { |
Check warning
Code scanning / CodeQL
Indirect uncontrolled command line Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
In general, to fix this class of issues, avoid passing concatenated strings built from user-controlled data into child_process.exec/execSync (which invoke a shell). Instead, use spawn/spawnSync or execFile/execFileSync with an explicit argument array, so that each argument is passed as-is without shell interpretation. If arguments are received as a single string, use a proper parser (like shell-quote) rather than manual splitting; here, we already have the argument as a structured value (packageName), so we can build the argument array ourselves.
For this specific code, the safest and least invasive change is:
- Replace the construction of the single
commandstring and theexecSync(command, ...)call with a call toexecFileSync(orspawnSync) where:- The command is
'pnpm'. - The arguments are an array such as
['dev', ...(packageName === 'all' ? [] : ['--filter',@objectstack/${packageName}])].
- The command is
- Keep the existing logging behavior by still building a
commandstring for display only, but do not pass that string to a shell. - Update the import from
child_processto importexecFileSyncinstead of (or in addition to)execSync.
Concretely in packages/cli/src/commands/dev.ts:
- Change
import { execSync } from 'child_process';toimport { execFileSync } from 'child_process';. - Keep the “display only” string
command = \pnpm ${filter} dev`` (or adjust ordering to match the real args) for logging, but no longer use it as the argument to a shell. - Introduce an
argsarray:This preserves the intended functionality (runningconst args = ['dev']; if (filter) { args.push('--filter', `@objectstack/${packageName}`); } execFileSync('pnpm', args, { stdio: 'inherit', cwd });
pnpm devoptionally with--filter @objectstack/<packageName>) while eliminating shell interpretation ofpackageName.
No other files or logic need to change.
| @@ -1,6 +1,6 @@ | ||
| import { Command } from 'commander'; | ||
| import chalk from 'chalk'; | ||
| import { execSync } from 'child_process'; | ||
| import { execFileSync } from 'child_process'; | ||
| import os from 'os'; | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| @@ -16,18 +16,24 @@ | ||
|
|
||
| try { | ||
| const cwd = process.cwd(); | ||
| const filter = packageName === 'all' ? '' : `--filter @objectstack/${packageName}`; | ||
| const filter = packageName === 'all' ? '' : `@objectstack/${packageName}`; | ||
|
|
||
| console.log(`📦 Package: ${chalk.blue(packageName === 'all' ? 'All packages' : `@objectstack/${packageName}`)}`); | ||
| console.log(`🔄 Watch mode: ${chalk.green('enabled')}`); | ||
| console.log(''); | ||
|
|
||
| // Start dev mode | ||
| const command = `pnpm ${filter} dev`.trim(); | ||
| const command = packageName === 'all' | ||
| ? 'pnpm dev' | ||
| : `pnpm --filter ${filter} dev`; | ||
| console.log(chalk.dim(`$ ${command}`)); | ||
| console.log(''); | ||
|
|
||
| const args = packageName === 'all' | ||
| ? ['dev'] | ||
| : ['--filter', filter, 'dev']; | ||
|
|
||
| execSync(command, { | ||
| execFileSync('pnpm', args, { | ||
| stdio: 'inherit', | ||
| cwd | ||
| }); |
| import chalk from 'chalk'; | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import { execSync } from 'child_process'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
In general, the correct fix for an unused import is to remove the import statement (or the unused specifier from a multi-specifier import) so that the code only imports what it actually uses. This improves readability and avoids misleading future maintainers into thinking the symbol is important.
Here, the best fix without changing functionality is to delete the unused execSync import line from packages/cli/src/commands/create.ts. No other code references execSync in the shown region, and removing the import does not affect runtime behavior. Specifically, in packages/cli/src/commands/create.ts, remove line 5 (import { execSync } from 'child_process';). No new methods, variables, or imports are needed.
| @@ -2,7 +2,6 @@ | ||
| import chalk from 'chalk'; | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import { execSync } from 'child_process'; | ||
|
|
||
| const templates = { | ||
| plugin: { |
| import { Command } from 'commander'; | ||
| import chalk from 'chalk'; | ||
| import { execSync } from 'child_process'; | ||
| import os from 'os'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
In general, unused imports should be removed entirely to make the code clearer and avoid misleading future maintainers into thinking they are needed. This also ensures static analysis tools and linters pass cleanly.
For this specific file, the best fix is simply to delete the unused import os from 'os'; line at the top of packages/cli/src/commands/dev.ts. No other code changes are required because nothing in the file references os. The remaining imports (Command, chalk, execSync, fs, path) continue to function as before. No additional methods, imports, or definitions are needed.
| @@ -1,7 +1,6 @@ | ||
| import { Command } from 'commander'; | ||
| import chalk from 'chalk'; | ||
| import { execSync } from 'child_process'; | ||
| import os from 'os'; | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
| import chalk from 'chalk'; | ||
| import { execSync } from 'child_process'; | ||
| import os from 'os'; | ||
| import fs from 'fs'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
In general, unused imports should be removed to keep the codebase clean and avoid confusion about dependencies. If the imported module is no longer needed, deleting the import line is the safest fix and does not change runtime behavior, as standard Node.js fs imports are side-effect free.
For this file, the best fix is to delete the fs import on line 5 in packages/cli/src/commands/dev.ts. No other changes are needed: no functions or variables reference fs, and removing the import does not affect the devCommand behavior. We do not need to add any new imports or adjust existing logic.
| @@ -2,7 +2,6 @@ | ||
| import chalk from 'chalk'; | ||
| import { execSync } from 'child_process'; | ||
| import os from 'os'; | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
| export const devCommand = new Command('dev') |
| import { execSync } from 'child_process'; | ||
| import os from 'os'; | ||
| import fs from 'fs'; | ||
| import path from 'path'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
To fix the problem, remove the unused path import. This resolves the CodeQL warning and slightly simplifies the module without changing runtime behavior.
Concretely, in packages/cli/src/commands/dev.ts, delete the line import path from 'path'; (line 6). No additional code changes or new imports are needed, since path is never referenced in the rest of the file. The remaining imports (Command, chalk, execSync, os, fs) stay as they are because we have not been told they are unused.
| @@ -3,8 +3,8 @@ | ||
| import { execSync } from 'child_process'; | ||
| import os from 'os'; | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
|
|
||
| export const devCommand = new Command('dev') | ||
| .description('Start development mode for a package') | ||
| .argument('[package]', 'Package name (without @objectstack/ prefix)', 'all') |
There was a problem hiding this comment.
Pull request overview
Adds a development toolchain to the ObjectStack monorepo by expanding @objectstack/cli with new commands (serve/dev/doctor/create), adding repo-level pnpm shortcuts, and documenting the intended workflows.
Changes:
- Added new CLI commands:
serve,dev,doctor, andcreate(plugin/example scaffolding). - Updated repo scripts (
pnpm doctor,pnpm setup,pnpm test, etc.) and added new developer docs (DEVELOPMENT.md,TOOLCHAIN_DEMO.md). - Updated dependencies/lockfile to support server startup and plugin loading.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds workspace deps for CLI to use core + hono server plugin. |
| packages/cli/package.json | Adds @objectstack/core + @objectstack/plugin-hono-server dependencies for new commands. |
| packages/cli/src/bin.ts | Registers new CLI commands and updates CLI metadata/version. |
| packages/cli/src/commands/serve.ts | Implements objectstack serve for starting a kernel and optionally an HTTP server plugin. |
| packages/cli/src/commands/dev.ts | Implements objectstack dev wrapper around pnpm --filter ... dev. |
| packages/cli/src/commands/doctor.ts | Implements objectstack doctor environment checks. |
| packages/cli/src/commands/create.ts | Implements objectstack create scaffolding for plugin/example templates. |
| package.json | Adds repo-level doctor, setup, test shortcuts. |
| README.md | Updates quickstart to include pnpm doctor and links to development guide. |
| DEVELOPMENT.md | Adds development/debugging/testing guide and CLI usage docs. |
| TOOLCHAIN_DEMO.md | Demonstrates the new tooling and intended workflows. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| for (const plugin of plugins) { | ||
| try { | ||
| kernel.registerPlugin(plugin); | ||
| const pluginName = plugin.name || plugin.constructor?.name || 'unnamed'; | ||
| console.log(chalk.green(` ✓ Registered plugin: ${pluginName}`)); | ||
| } catch (e: any) { | ||
| console.error(chalk.red(` ✗ Failed to register plugin: ${e.message}`)); | ||
| } |
There was a problem hiding this comment.
The core kernel API uses use(plugin) to register plugins (see packages/core/src/kernel.ts:34), but this command calls kernel.registerPlugin(...), which doesn’t exist. Switch to the correct registration method (and consider failing fast if a required plugin can’t be registered).
| dependencies: { | ||
| '@objectstack/spec': 'workspace:*', | ||
| zod: '^3.22.4', | ||
| }, | ||
| devDependencies: { | ||
| '@types/node': '^20.10.0', | ||
| typescript: '^5.3.0', | ||
| vitest: '^2.1.8', | ||
| }, |
There was a problem hiding this comment.
The generated plugin template imports/implements the runtime Plugin interface, but its package.json template doesn’t depend on @objectstack/core (which is where the runtime Plugin interface used by existing plugins lives). Add @objectstack/core as a dependency (like packages/plugins/plugin-hono-server/package.json) and align the template’s imports accordingly.
| 'src/index.ts': (name: string) => `import type { Plugin } from '@objectstack/spec'; | ||
|
|
||
| /** | ||
| * ${name} Plugin for ObjectStack | ||
| */ | ||
| export const ${toCamelCase(name)}Plugin: Plugin = { | ||
| name: '${name}', | ||
| version: '0.1.0', | ||
|
|
||
| async initialize(context) { | ||
| console.log('Initializing ${name} plugin...'); | ||
| // Plugin initialization logic | ||
| }, | ||
|
|
||
| async destroy() { | ||
| console.log('Destroying ${name} plugin...'); | ||
| // Plugin cleanup logic | ||
| }, | ||
| }; | ||
|
|
||
| export default ${toCamelCase(name)}Plugin; | ||
| `, |
There was a problem hiding this comment.
The generated src/index.ts template won’t type-check against the actual plugin lifecycle: @objectstack/spec does not export Plugin at the root (see packages/spec/src/index.ts), and the runtime plugin interface expects init(ctx) (required) / start?(ctx) / destroy?() (see packages/core/src/types.ts:65). Update the template to import Plugin (and PluginContext if needed) from @objectstack/core (or @objectstack/spec/contracts if you intend the contracts interface), and rename initialize → init (and optionally add start).
| export const ${toCamelCase(name)}Plugin: Plugin = { | ||
| name: '${name}', | ||
| version: '0.1.0', | ||
|
|
There was a problem hiding this comment.
The template sets the plugin name to the raw project name (e.g. auth), but existing runtime plugins use a reverse-domain style identifier (e.g. packages/objectql/src/plugin.ts:8 → com.objectstack.engine.objectql, packages/plugins/plugin-hono-server/src/hono-plugin.ts:17 → com.objectstack.server.hono). Using short names increases the chance of collisions. Consider generating a reverse-domain-style id by default (or at least documenting/enforcing uniqueness).
| const cwd = process.cwd(); | ||
| const filter = packageName === 'all' ? '' : `--filter @objectstack/${packageName}`; | ||
|
|
||
| console.log(`📦 Package: ${chalk.blue(packageName === 'all' ? 'All packages' : `@objectstack/${packageName}`)}`); | ||
| console.log(`🔄 Watch mode: ${chalk.green('enabled')}`); | ||
| console.log(''); | ||
|
|
||
| // Start dev mode | ||
| const command = `pnpm ${filter} dev`.trim(); | ||
| console.log(chalk.dim(`$ ${command}`)); | ||
| console.log(''); | ||
|
|
||
| execSync(command, { | ||
| stdio: 'inherit', | ||
| cwd |
There was a problem hiding this comment.
execSync() is invoked with a shell command string that interpolates the user-provided packageName into --filter @objectstack/${packageName}. This allows shell injection if packageName contains characters like ;/&&. Prefer spawn/execFile with an argument array (no shell), and validate packageName against an allowlist regex (e.g., ^[a-z0-9-]+$ or all).
| const baseDir = type === 'plugin' ? 'packages/plugins' : 'examples'; | ||
| const projectName = type === 'plugin' ? `plugin-${name}` : name; | ||
| targetDir = path.join(cwd, baseDir, projectName); | ||
| } |
There was a problem hiding this comment.
name is interpolated into projectName and then passed to path.join(...) without sanitizing path separators. Inputs like ../../evil can cause the normalized targetDir to escape the intended packages/plugins or examples base directory. Reject names containing / or \ (and .. segments) before building paths, or normalize and verify targetDir stays within the expected base dir.
| @@ -0,0 +1,397 @@ | |||
| # 开发调试测试指南 | Development, Debugging & Testing Guide | |||
|
|
|||
| > **English** | [中文](#中文版) | |||
There was a problem hiding this comment.
The link [中文](#中文版) points to an anchor that doesn’t exist in this document (there’s no # 中文版 section). Either add the referenced section or remove/update the link to avoid broken navigation.
| > **English** | [中文](#中文版) | |
| > **English** | 中文 |
| description: `ObjectStack Example: ${name}`, | ||
| scripts: { | ||
| build: 'objectstack compile', | ||
| dev: 'tsx watch objectstack.config.ts', |
There was a problem hiding this comment.
In the generated example template, the dev script is tsx watch objectstack.config.ts, but objectstack.config.ts only exports a stack definition and has no side effects—so this “dev” command will just exit immediately (and keep re-running on changes without doing anything). Consider changing the template’s dev workflow to something actionable (e.g., objectstack compile in watch mode if supported, or objectstack serve if the goal is to run a server).
| dev: 'tsx watch objectstack.config.ts', | |
| dev: 'objectstack compile', |
| // Load plugins from configuration | ||
| const plugins = config.plugins || []; | ||
|
|
||
| if (plugins.length > 0) { | ||
| console.log(chalk.yellow(`📦 Loading ${plugins.length} plugin(s)...`)); | ||
|
|
||
| for (const plugin of plugins) { | ||
| try { | ||
| kernel.registerPlugin(plugin); | ||
| const pluginName = plugin.name || plugin.constructor?.name || 'unnamed'; | ||
| console.log(chalk.green(` ✓ Registered plugin: ${pluginName}`)); | ||
| } catch (e: any) { | ||
| console.error(chalk.red(` ✗ Failed to register plugin: ${e.message}`)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Add HTTP server plugin if not disabled | ||
| if (options.server !== false) { | ||
| try { | ||
| const { HonoServerPlugin } = await import('@objectstack/plugin-hono-server'); | ||
| const serverPlugin = new HonoServerPlugin({ port: parseInt(options.port) }); | ||
| kernel.registerPlugin(serverPlugin); | ||
| console.log(chalk.green(` ✓ Registered HTTP server plugin (port: ${options.port})`)); | ||
| } catch (e: any) { | ||
| console.warn(chalk.yellow(` ⚠ HTTP server plugin not available: ${e.message}`)); | ||
| } | ||
| } |
There was a problem hiding this comment.
--no-server currently only skips auto-registering HonoServerPlugin, but plugins from config.plugins are always registered. If a user includes new HonoServerPlugin(...) in config.plugins, --no-server won’t actually run headless. Consider either filtering out known HTTP server plugins when --no-server is set, or clarifying the flag’s behavior in the help text/docs.
| const { ObjectStackKernel } = await import('@objectstack/core'); | ||
|
|
||
| // Create kernel instance | ||
| console.log(chalk.yellow(`🔧 Initializing ObjectStack kernel...`)); | ||
| const kernel = new ObjectStackKernel({ | ||
| metadata: config.metadata || {}, | ||
| objects: config.objects || {}, | ||
| }); | ||
|
|
||
| // Load plugins from configuration | ||
| const plugins = config.plugins || []; |
There was a problem hiding this comment.
@objectstack/core does not export ObjectStackKernel (only ObjectKernel / EnhancedObjectKernel). This import will fail at runtime, and the constructor signature used here (metadata/objects) doesn’t match the core kernels. Update to the actual kernel class exported by @objectstack/core and initialize it using the supported constructor options (e.g., logger config).
| const { ObjectStackKernel } = await import('@objectstack/core'); | |
| // Create kernel instance | |
| console.log(chalk.yellow(`🔧 Initializing ObjectStack kernel...`)); | |
| const kernel = new ObjectStackKernel({ | |
| metadata: config.metadata || {}, | |
| objects: config.objects || {}, | |
| }); | |
| // Load plugins from configuration | |
| const plugins = config.plugins || []; | |
| const { EnhancedObjectKernel } = await import('@objectstack/core'); | |
| // Create kernel instance | |
| console.log(chalk.yellow(`🔧 Initializing ObjectStack kernel...`)); | |
| const kernel = new EnhancedObjectKernel(); | |
| // Load plugins from configuration | |
| const plugins = config.plugins || []; | |
| // Load plugins from configuration | |
| const plugins = config.plugins || []; |
Developing plugins and packages required manual scaffolding, environment setup, and boilerplate generation. No automated health checks or standardized templates existed. Additionally, there was no command to start a server with plugin configuration.
Changes
CLI Commands (
@objectstack/cli)objectstack compile [config]- Compile configuration to JSON definitionobjectstack serve [config]- Start ObjectStack server with plugins from configurationobjectstack dev [package]- Watch mode for package developmentobjectstack doctor- Environment health check with fix suggestionsobjectstack create {plugin|example} <name>- Template-based project scaffoldingServer Command Features
config.pluginsarray in configuration file--port <port>flag (default: 3000)--no-serverflag to skip HTTP serverProject Templates
Developer Experience
pnpm doctor,pnpm setup,pnpm test,pnpm build,pnpm cleanshortcutsUsage
Server Configuration Example:
Impact
Note: Implementation uses CLI-only approach for better cross-platform compatibility and maintainability. All functionality is provided through
pnpm objectstackcommands and npm shortcuts.Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.