diff --git a/.changeset/22156-cli-port-help-and-init-next-steps.md b/.changeset/22156-cli-port-help-and-init-next-steps.md new file mode 100644 index 00000000000..096eb4aecae --- /dev/null +++ b/.changeset/22156-cli-port-help-and-init-next-steps.md @@ -0,0 +1,11 @@ +--- +"@objectstack/cli": patch +--- + +`os dev --help` and `os start --help` name `OS_PORT` for `--port`, and `os init --no-install` names the package manager it resolved in its Next steps. + +Clause-②: no + +- **`os dev --port`.** The help text read `Server port (overrides $PORT)`, though `os dev` reads `OS_PORT` first and `PORT` only as its legacy alias. It now reads `Server port (overrides $OS_PORT; $PORT is the legacy alias)`. Which variables are read, and in what order, is unchanged. +- **`os start --port`.** The same drift: the help text read `Port to listen on (overrides $PORT, default 3000)`, though `os start` reads `OS_PORT` first as well. It now reads `Port to listen on, default 3000 (overrides $OS_PORT; $PORT is the legacy alias)`. The port behaviour is unchanged. +- **`os init --no-install`.** The package manager was resolved only when an install ran, so a `--no-install` run always printed `npm install` and `npx objectstack …`, even with `--package-manager pnpm` or when invoked through pnpm. It is now resolved before the install step, the same way as before (the `--package-manager` flag, then the invoking package manager, then npm), so `os init my-app --no-install --package-manager pnpm` prints `pnpm install` and `pnpm exec objectstack …`. A run with no flag, invoked through npm or `npx`, still prints `npm install`: the scaffold supports npm, yarn and bun as well as pnpm. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1d533d0f2e7..f4dbca1fca5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,447 +1,158 @@ -# Contributing to ObjectStack Protocol +# Contributing to ObjectStack -Thank you for your interest in contributing to ObjectStack! This guide will help you get started with contributing to the protocol specifications. +Thank you for your interest in contributing to ObjectStack! This repository is the +whole open stack, not only the protocol: the Zod protocol (`packages/spec`), the +microkernel, the runtime, the drivers, plugins and services, the CLI, the client SDK, +the example apps and the documentation site. This guide takes you from a fork to a +pull request. + +**[AGENTS.md](./AGENTS.md) is the rulebook, for human contributors and coding agents +alike.** It holds naming, the Zod-first and contract-first rules, the documentation +guardrails and the changeset rules. This file does not restate them. Where a rule +matters, it points you to AGENTS.md, and if the two ever disagree, AGENTS.md wins. ## 📋 Table of Contents - [Code of Conduct](#code-of-conduct) - [Getting Started](#getting-started) -- [Development Workflow](#development-workflow) -- [Contribution Types](#contribution-types) -- [Coding Standards](#coding-standards) -- [Testing Guidelines](#testing-guidelines) -- [Documentation Guidelines](#documentation-guidelines) +- [Making a Change](#making-a-change) +- [Where Things Live](#where-things-live) - [Pull Request Process](#pull-request-process) - [Community](#community) ## Code of Conduct -We are committed to providing a welcoming and inclusive environment. Please be respectful and professional in all interactions. +We are committed to providing a welcoming and inclusive environment. Please read the +[Code of Conduct](./CODE_OF_CONDUCT.md) and be respectful and professional in all +interactions. ## Getting Started ### Prerequisites -- **Node.js** >= 22.0.0 -- **PNPM** >= 8.0.0 -- **Git** >= 2.0.0 +- **Node.js** 22 or later (`engines.node` in the root `package.json`) +- **pnpm 10**. `corepack enable` installs the exact version the root `package.json` + pins in `packageManager`, and a pnpm 10 you installed yourself switches to that + pinned version inside this repository. pnpm 8 and 9 cannot install this workspace: + pnpm 8 refuses the lockfile, and pnpm 9 stops a frozen install on the `overrides` + that live in `pnpm-workspace.yaml`. +- **Git** ### Initial Setup ```bash -# 1. Fork the repository on GitHub +# 1. Fork objectstack-ai/objectstack on GitHub # 2. Clone your fork -git clone https://github.com/YOUR_USERNAME/spec.git -cd spec +git clone https://github.com/YOUR_USERNAME/objectstack.git +cd objectstack -# 3. Add upstream remote -git remote add upstream https://github.com/objectstack-ai/spec.git +# 3. Add the upstream remote +git remote add upstream https://github.com/objectstack-ai/objectstack.git # 4. Install dependencies +corepack enable pnpm install -# 5. Build the project -pnpm build - -# 6. Run tests -pnpm test -``` - -## Development Workflow - -### 1. Choose What to Work On - -Before starting, review: -- **[PRIORITIES.md](./internal/planning/PRIORITIES.md)** - Current sprint priorities -- **[DEVELOPMENT_ROADMAP.md](./internal/planning/DEVELOPMENT_ROADMAP.md)** - Long-term roadmap -- **[GitHub Issues](https://github.com/objectstack-ai/spec/issues)** - Open issues - -### 2. Create a Branch - -```bash -# Update your main branch -git checkout main -git pull upstream main - -# Create a feature branch -git checkout -b feature/your-feature-name -# or -git checkout -b fix/your-bug-fix -``` - -### 3. Make Your Changes - -Follow the [Coding Standards](#coding-standards) and ensure: -- Changes are minimal and focused -- Code follows existing patterns -- Tests are added/updated -- Documentation is updated - -### 4. Test Your Changes - -```bash -# Run all tests -pnpm test - -# Run tests for specific package -pnpm --filter @objectstack/spec test - -# Build to verify schemas +# 5. Build the workspace (nothing builds it implicitly) pnpm build ``` -### 5. Submit Your Changes - -```bash -# Stage your changes -git add . - -# Commit with descriptive message -git commit -m "feat: add new field type for encrypted data" - -# Push to your fork -git push origin feature/your-feature-name - -# Create Pull Request on GitHub -``` - -## Contribution Types - -### 🔧 Protocol Definitions - -Adding or modifying protocol definitions in `packages/spec/src/`: - -1. **Create Zod Schema** - Always start here -2. **Add JSDoc Comments** - Document with `@description` -3. **Write Tests** - Target 80%+ coverage -4. **Generate Schemas** - Run `pnpm build` -5. **Create Documentation** - Add MDX in `content/docs/references/` - -> **Single Source of Truth — One Zod schema per metadata type.** -> Each metadata type (`view`, `dashboard`, `flow`, `agent`, `tool`, `object`, …) -> has exactly **one** Zod schema under `packages/spec/src/{domain}/`. Do -> **not** re-declare the same shape as a `*.object.ts` projection table — -> that pattern was removed in ADR-0005 (see `docs/adr/0005-…`). Studio -> editing forms and the overlay validator (`resolveOverlaySchema()` in -> `packages/objectql/src/protocol.ts`) both bind to that one schema. -> -> **Runtime opt-in for org overlays** lives in exactly one place: the -> `allowOrgOverride` boolean on the type's entry in -> `DEFAULT_METADATA_TYPE_REGISTRY` -> (`packages/spec/src/kernel/metadata-plugin.zod.ts`). Do not maintain a -> parallel whitelist in runtime code. - -Example: -```typescript -/** - * Represents an encrypted field for storing sensitive data - * @description Provides end-to-end encryption for sensitive information - */ -export const EncryptedFieldSchema = z.object({ - /** Field type identifier */ - type: z.literal('encrypted'), - - /** Encryption algorithm (default: AES-256-GCM) */ - algorithm: z.enum(['aes-256-gcm', 'rsa-4096']).default('aes-256-gcm'), - - /** Key management strategy */ - keyManagement: z.enum(['user', 'organization', 'system']).default('organization'), -}); -``` - -### 📚 Documentation - -- **Concepts** - High-level explanations in `content/docs/concepts/` -- **Guides** - How-to tutorials in `content/docs/guides/` -- **References** - API documentation in `content/docs/references/` -- **Specifications** - Protocol specs in `content/docs/specifications/` - -### 🐛 Bug Fixes - -1. Create an issue describing the bug (if not exists) -2. Reference the issue in your PR -3. Add regression tests -4. Update documentation if behavior changes - -### ✨ Examples - -Add working examples in `examples/`: -- Include `README.md` with setup instructions -- Provide `objectstack.config.ts` configuration -- Add `CHANGELOG.md` for version history - -## Coding Standards - -### Naming Conventions - -**CRITICAL**: Follow these naming conventions strictly: - -#### Configuration Keys (TypeScript Properties) -Use `camelCase`: -```typescript -{ - maxLength: 100, - defaultValue: 'none', -} -``` - -#### Machine Names (Data Values) -Use `snake_case`: -```typescript -{ - name: 'project_task', - object: 'account', - field: 'first_name', -} -``` - -### Schema Definition Pattern - -```typescript -import { z } from 'zod'; - -/** - * Schema description - * @description Detailed explanation - */ -export const MySchema = z.object({ - /** Property description */ - propertyName: z.string().describe('Property description'), - - /** Another property */ - anotherProperty: z.number().optional().describe('Optional property'), -}); - -export type MyType = z.infer; -``` - -### File Organization - -``` -packages/spec/src/ -├── data/ # ObjectQL - Data Protocol -│ ├── field.zod.ts -│ ├── object.zod.ts -│ └── validation.zod.ts -├── ui/ # ObjectUI - UI Protocol -│ ├── app.zod.ts -│ ├── view.zod.ts -│ └── theme.zod.ts -├── system/ # ObjectOS - System Protocol -│ ├── manifest.zod.ts -│ ├── plugin.zod.ts -│ └── driver.zod.ts -├── ai/ # AI Protocol -│ ├── agent.zod.ts -│ └── model.zod.ts -└── api/ # API Protocol - ├── envelopes.zod.ts - └── requests.zod.ts -``` - -## Testing Guidelines - -### Test Coverage - -- **Target**: 80%+ code coverage -- **Location**: Co-located `*.test.ts` files -- **Framework**: Vitest - -### Test Structure - -```typescript -import { describe, it, expect } from 'vitest'; -import { MySchema } from './my-schema.zod'; - -describe('MySchema', () => { - describe('validation', () => { - it('should accept valid data', () => { - const result = MySchema.safeParse({ - propertyName: 'valid value', - }); - expect(result.success).toBe(true); - }); - - it('should reject invalid data', () => { - const result = MySchema.safeParse({ - propertyName: 123, // wrong type - }); - expect(result.success).toBe(false); - }); - }); - - describe('type inference', () => { - it('should infer correct TypeScript types', () => { - type MyType = z.infer; - const data: MyType = { - propertyName: 'value', - }; - expect(data.propertyName).toBe('value'); - }); - }); -}); -``` - -## Documentation Guidelines - -### MDX Documentation - -Create documentation in `content/docs/references/` matching the source structure: - -```markdown ---- -title: MySchema -description: Schema description for SEO and navigation ---- - -# MySchema - -Brief description of what this schema represents. - -## Overview - -Detailed explanation of the schema's purpose and use cases. - -## Schema Definition - -\`\`\`typescript -import { MySchema } from '@objectstack/spec'; - -const config = { - propertyName: 'value', -}; - -const validated = MySchema.parse(config); -\`\`\` - -## Properties - -### propertyName - -- **Type**: `string` -- **Required**: Yes -- **Description**: Description of this property - -## Examples - -### Basic Usage - -\`\`\`typescript -const basic = { - propertyName: 'simple value', -}; -\`\`\` - -### Advanced Usage - -\`\`\`typescript -const advanced = { - propertyName: 'complex value', - anotherProperty: 42, -}; -\`\`\` - -## Related - -- [RelatedSchema](./related-schema) -- [AnotherSchema](./another-schema) -``` - -### Bilingual Support - -Provide both English and Chinese versions: -- English: `my-schema.mdx` -- Chinese: `my-schema.cn.mdx` +The README's [Hack on the framework](./README.md#hack-on-the-framework) section +covers the rest: building the Console, running an example app with `pnpm dev`, and +how long a first build and a full test run take. + +## Making a Change + +1. **Start from an issue.** Pick one from the + [issue tracker](https://github.com/objectstack-ai/objectstack/issues), or open one + that describes the bug or the proposal before you write code. + [ROADMAP.md](./ROADMAP.md) and [docs/NORTH-STAR.md](./docs/NORTH-STAR.md) show + where the project is going. +2. **Branch off an up-to-date `main`, one branch and one pull request per issue**, + with the issue number in the branch name: + + ```bash + git fetch upstream + git checkout -b fix/issue-1234-short-slug upstream/main + ``` + + Working in a checkout that coding agents also use? Give every task its own + `git worktree` instead of switching branches in a shared tree. AGENTS.md + (Prime Directive #11 and *Multi-agent working discipline*) explains why, and what + a worktree does not isolate. +3. **Make the change, following AGENTS.md.** Read its *Prime Directives* before a + structural change, and its *Context Routing* table for the rules of the path you + are editing. +4. **Test what your change reaches**, not only the file you edited: + + ```bash + pnpm --filter @objectstack/ test # one package + pnpm --filter @objectstack/ typecheck # a type-check-covered package + pnpm turbo run test --affected # every package your branch reaches + pnpm lint # the only style authority (no formatter) + ``` + + Did you touch `packages/spec`? Then regenerate its checked-in artifacts before you + push. The steps are in AGENTS.md under *Touched `packages/spec`?*. +5. **Add a changeset when the change publishes.** Run `pnpm changeset` and commit the + `.changeset/*.md` file it writes. A bug fix in a released package takes a `patch` + changeset. AGENTS.md's *Post-Task Checklist* (step 3) states the whole rule: which + bump a change takes, the `Clause-②` declaration, and the migration a breaking + change must carry. + +## Where Things Live + +| Path | What it holds | +|:---|:---| +| `packages/spec/src/` | The protocol: Zod schemas, types and constants. AGENTS.md lists the domains. | +| `packages/`, `packages/plugins/`, `packages/services/`, … | The kernel, runtime, drivers, plugins, services, CLI and SDK. The README's package directory has them all. | +| `content/docs/` | The documentation site's pages, in trees such as `getting-started/`, `concepts/`, `data-modeling/`, `ui/`, `automation/`, `api/` and `deployment/`. Preview with `pnpm docs:dev`. | +| `content/docs/references/` | Generated from the Zod schemas by `packages/spec/scripts/build-docs.ts`. Never edit it by hand. | +| `content/docs/releases/` | Written at release time from changesets. Never edit it in a code pull request. | +| `examples/` | The example apps. The README's *Examples* table describes each one. | +| [ARCHITECTURE.md](./ARCHITECTURE.md) | Design details, the plugin lifecycle and the dependency graph. | + +AGENTS.md (*Documentation Guardrails*) has the rules for every docs path, including what a +new page needs before it shows up in the navigation. ## Pull Request Process ### Before Submitting -- [ ] All tests pass (`pnpm test`) -- [ ] Code builds successfully (`pnpm build`) -- [ ] Documentation is updated -- [ ] Naming conventions are followed -- [ ] JSDoc comments are complete -- [ ] No unrelated changes included +- [ ] The tests and type-check for what your change reaches pass +- [ ] `pnpm lint` passes +- [ ] Documentation is updated where behaviour changed +- [ ] A changeset is included if the change publishes +- [ ] No unrelated changes are included - [ ] If the PR changes the auth/audience **defaults** or the **accept/reject behaviour** of the unauthenticated surface, label it `needs:pack-smoke` — that runs the packed-install smoke on the merge preview before you merge, instead of finding out at release time. -### PR Checklist - -Use this template for your PR description: - -```markdown -## Description -Brief description of changes - -## Type of Change -- [ ] Bug fix -- [ ] New feature -- [ ] Breaking change -- [ ] Documentation update - -## Changes Made -- Item 1 -- Item 2 - -## Testing -- [ ] Unit tests added/updated -- [ ] All tests passing -- [ ] Manual testing completed - -## Documentation -- [ ] JSDoc comments added -- [ ] MDX documentation created/updated -- [ ] Examples provided - -## Checklist -- [ ] Zod schema follows naming conventions -- [ ] Comprehensive JSDoc comments with @description -- [ ] Unit tests with 80%+ coverage -- [ ] Documentation with examples -- [ ] JSON schema generated successfully -- [ ] All existing tests pass -``` +### Opening the Pull Request + +Push your branch to your fork and open a pull request against `main`. Its first line +names the issue it fixes (`Fixes #1234`). The body says what changed, why, and how you +verified it. ### Review Process -1. **Automated Checks** - CI/CD runs tests and builds -2. **Code Review** - Maintainers review your code -3. **Feedback** - Address review comments -4. **Approval** - At least one maintainer approval required -5. **Merge** - Maintainers will merge when ready +1. **Automated checks:** CI runs the repository gates, the type-check, the tests and the + builds. +2. **Code review:** maintainers review your change. +3. **Feedback:** address the review comments. +4. **Merge:** maintainers land the pull request through the merge queue. ## Community -### Communication Channels - -- **GitHub Discussions** - General questions and discussions -- **GitHub Issues** - Bug reports and feature requests -- **Pull Requests** - Code contributions - -### Getting Help - -- Review [PLANNING_INDEX.md](./internal/planning/PLANNING_INDEX.md) for documentation navigation -- Check [ARCHITECTURE.md](./ARCHITECTURE.md) for system design -- Read [QUICK_START_IMPLEMENTATION.md](./QUICK_START_IMPLEMENTATION.md) for implementation examples - -### Recognition - -Contributors will be: -- Listed in release notes -- Mentioned in the CHANGELOG -- Credited in documentation (where applicable) +- **[GitHub Discussions](https://github.com/objectstack-ai/objectstack/discussions)** — + questions and ideas +- **[GitHub Issues](https://github.com/objectstack-ai/objectstack/issues)** — bug reports + and feature requests +- **[Documentation](https://objectstack.ai/docs)** — the published docs site ## License By contributing, you agree that your contributions will be licensed under the -Apache License, Version 2.0. - ---- - -**Questions?** Open a [GitHub Discussion](https://github.com/objectstack-ai/spec/discussions) - -**Need Help?** Check the [documentation](./content/docs/) or ask in discussions +Apache License, Version 2.0 (see [LICENSE](./LICENSE) and [LICENSING.md](./LICENSING.md)). **Thank you for contributing to ObjectStack! 🚀** diff --git a/README.md b/README.md index 7d9816bd07d..a87fc210cef 100644 --- a/README.md +++ b/README.md @@ -136,10 +136,16 @@ export const Ticket = ObjectSchema.create({ }); ``` -The REST API exists the moment the object does — no controllers to write: +The REST API exists the moment the object does — no controllers to write. It +runs under the same permissions as the UI, so a data call needs a session: sign +in once as the dev admin `os dev` seeds on an empty database, then call it. ```bash -curl http://localhost:3000/api/v1/data/support_desk_ticket +curl -c cookies.txt -X POST http://localhost:3000/api/v1/auth/sign-in/email \ + -H "Content-Type: application/json" \ + -d '{"email":"admin@objectos.ai","password":"admin123"}' + +curl -b cookies.txt http://localhost:3000/api/v1/data/support_desk_ticket ``` In the browser, the typed client SDK and React hooks (`useQuery`, `useMutation`, diff --git a/content/docs/getting-started/build-with-claude-code.mdx b/content/docs/getting-started/build-with-claude-code.mdx index b226f225f3d..9da7c9016a1 100644 --- a/content/docs/getting-started/build-with-claude-code.mdx +++ b/content/docs/getting-started/build-with-claude-code.mdx @@ -96,8 +96,8 @@ language: > description, a `priority` select (low/normal/high/urgent) and a `status` select > (open/pending/resolved/closed). Add a **Resolve** action that only shows on > tickets that aren't already resolved or closed. Add a list view (subject, -> status, priority) with an "Open tickets" filter, and an app with a Support nav -> group. Run `npm run validate` when you're done. +> status, priority) with an "Open tickets" filter, a form for entering tickets, +> and an app with a Support nav group. Run `npm run validate` when you're done. You don't specify field types in TypeScript, wire barrel exports, or remember the CEL scoping rules — the agent does, because the skills told it how. @@ -198,6 +198,17 @@ export const TicketViews = defineView({ sort: [{ field: 'priority', order: 'desc' }], }, }, + form: { + type: 'simple', + data, + sections: [ + { + name: 'ticket', + label: 'Ticket', + fields: [{ field: 'subject' }, { field: 'description' }, { field: 'priority' }, { field: 'status' }], + }, + ], + }, }); ``` @@ -238,6 +249,12 @@ passes an unregistered one, the `os dev` boot log warns fails when clicked (the [two paths](/docs/ui/actions#give-it-server-behavior--two-paths) a handler takes). +The view file also declares the create and edit **form**, and the form is where the +long-text `description` is entered and shown. The list columns leave it out, so the +form is the only metadata that names it. A field that no consumer names (a view, a form, a +flow, a formula or an action, among the others the rule counts) gets an advisory +`field-no-consumers` warning from `os validate`. + The agent also **exports each new file from its directory's barrel** — the object from `src/objects/index.ts`, and the action, view and app from `src/actions/index.ts`, `src/views/index.ts` and `src/apps/index.ts`. The @@ -313,6 +330,8 @@ Once it's clean: Data: 2 Objects 6 Fields UI: 1 Apps 1 Views 1 Actions + Logic: 0 Flows + Security: 0 Positions 0 Permissions Runtime: 3 plugins ``` diff --git a/content/docs/getting-started/examples.mdx b/content/docs/getting-started/examples.mdx index f3bd457e6e5..be43a3526cc 100644 --- a/content/docs/getting-started/examples.mdx +++ b/content/docs/getting-started/examples.mdx @@ -6,7 +6,7 @@ description: Run and explore the built-in example applications to learn ObjectSt import { CheckSquare, Building2, BarChart3, Server } from 'lucide-react'; -The monorepo ships three ready-to-run examples in `examples/` that progressively demonstrate ObjectStack features — from a simple Todo app to a full CRM and a kitchen-sink reference. For a larger external enterprise reference, see the [HotCRM repository](https://github.com/objectstack-ai/hotcrm). +The monorepo ships five examples in `examples/`. Three are ready-to-run apps that progressively demonstrate ObjectStack features — from a simple Todo app to a full CRM and a kitchen-sink reference — and each has its own `pnpm dev:*` script. The other two demonstrate a shape rather than a feature set: `app-multi-package` is one release artifact carrying two packages that share a namespace ([below](#a-project-is-a-multi-package-artifact)), and `embed-objectql` uses ObjectQL as a plain library, with no kernel and no plugins. For a larger external enterprise reference, see the [HotCRM repository](https://github.com/objectstack-ai/hotcrm). **Read these as reference, then build your own with an agent.** Each example is the same @@ -50,7 +50,7 @@ The fastest way to explore all examples at once: ```bash git clone https://github.com/objectstack-ai/objectstack.git cd objectstack -pnpm install # Node 22+, pnpm 8+ (corepack enable) +pnpm install # Node 22+, pnpm 10 (corepack enable) pnpm build # REQUIRED — nothing builds the workspace implicitly pnpm dev:showcase # or: pnpm dev:todo / pnpm dev:crm ``` diff --git a/content/docs/getting-started/index.mdx b/content/docs/getting-started/index.mdx index 4cc8b019709..a48e7949cf3 100644 --- a/content/docs/getting-started/index.mdx +++ b/content/docs/getting-started/index.mdx @@ -173,7 +173,8 @@ scaffolds a project with its own TypeScript toolchain and npm scripts. **pnpm and TypeScript versions only matter for the framework monorepo.** If you clone [`objectstack-ai/objectstack`](https://github.com/objectstack-ai/objectstack) itself (to contribute, or to run the bundled examples), you'll additionally need -pnpm 8+ (`corepack enable` pulls the pinned 10.x) — and the repo builds against +pnpm 10 (`corepack enable` installs the version the repo pins in `packageManager`; +pnpm 8 and 9 cannot install the workspace) — and the repo builds against TypeScript 6.x. diff --git a/content/docs/getting-started/your-first-project.mdx b/content/docs/getting-started/your-first-project.mdx index fe35e76927c..8dc1367c172 100644 --- a/content/docs/getting-started/your-first-project.mdx +++ b/content/docs/getting-started/your-first-project.mdx @@ -22,7 +22,7 @@ The two paths produce identical projects. | Tool | Minimum | Check | |:---|:---|:---| | **Node.js** | 22+ | `node --version` | -| A package manager | npm 9+ / pnpm 8+ / yarn / bun | `npm --version` | +| A package manager | npm 9+ / pnpm 10.15+ / yarn / bun | `npm --version` | That's all. A standalone project does **not** need pnpm, TypeScript pre-installed, or a database server — the default template runs on an in-memory driver, and diff --git a/examples/app-todo/README.md b/examples/app-todo/README.md index cc78fb40a22..b911ed97a59 100644 --- a/examples/app-todo/README.md +++ b/examples/app-todo/README.md @@ -98,7 +98,7 @@ examples/app-todo/ ## 💡 How to Run ### Prerequisites -- Node.js 22+ and pnpm 8+ +- Node.js 22+ and pnpm 10 (`corepack enable`) - Install from monorepo root: `corepack enable && pnpm install` ### Type Check diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 1c2f5e6156d..5ffd2e6d463 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -228,7 +228,7 @@ export default class Dev extends Command { description: 'Kernel logger level forwarded to `serve` (overrides $OS_LOG_LEVEL / $LOG_LEVEL; default `warn`). One of: debug | info | warn | error | fatal | silent.', options: ['debug', 'info', 'warn', 'error', 'fatal', 'silent'], }), - port: Flags.string({ char: 'p', description: 'Server port (overrides $PORT)' }), + port: Flags.string({ char: 'p', description: 'Server port (overrides $OS_PORT; $PORT is the legacy alias)' }), // #16804 — developer-supplied TLS, forwarded to the `serve` child. Declared // through the shared contract so the two commands cannot drift on the flag // names, the prose, or what half a pair means. diff --git a/packages/cli/src/commands/init-next-steps-package-manager.test.ts b/packages/cli/src/commands/init-next-steps-package-manager.test.ts new file mode 100644 index 00000000000..7fcd9d274a1 --- /dev/null +++ b/packages/cli/src/commands/init-next-steps-package-manager.test.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * `os init --no-install` names, in its printed Next steps, the package manager + * the run resolved: the `--package-manager` flag first, then the invoking + * package manager (`npm_config_user_agent`), then `npm`. + * + * The run used to resolve that package manager only inside its install branch. + * Under `--no-install` the branch never ran, the variable kept its literal + * `'npm'` initialiser, and the Next steps told a user who had passed + * `--package-manager pnpm` (or invoked the CLI through pnpm) to run + * `npm install` and `npx objectstack`. + * + * The third case is the control: a run invoked through npm, with no flag, still + * names npm. The scaffold deliberately supports npm, yarn and bun as well as pnpm + * (`engines.pnpm`, never a `packageManager` stamp; `init.test.ts` pins that), so + * `npm install` is the right instruction for an npm run, and this file must not + * read as "never npm". + * + * In-process (`Init.run` against the package root, the `doctor-*.test.ts` + * pattern, with its 60 s budget: the first oclif load in a busy worker took + * longer than vitest's 5 s default) and `--no-install`, so nothing is spawned + * and nothing is installed. + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import Init from './init.js'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +/** `packages/cli` — the oclif root the real command is loaded against below. */ +const CLI_ROOT = path.resolve(HERE, '..', '..'); + +/** Written as `\x1b`, never as the byte itself, so `grep` keeps reading this file as text. */ +const SGR = /\x1b\[[0-9;]*m/g; + +const NPM_UA = 'npm/10.9.4 node/v22.22.0 linux x64 workspaces/false'; +const PNPM_UA = 'pnpm/10.31.0 npm/? node/v22.22.0 linux x64'; + +describe('os init --no-install: the Next steps name the resolved package manager', () => { + let tmp: string; + let cwdSpy: ReturnType; + + beforeEach(() => { + tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-next-steps-pm-')); + cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tmp); + }); + + afterEach(() => { + cwdSpy.mockRestore(); + vi.unstubAllEnvs(); + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + /** Run `os init` in `tmp` and return the printed lines from `Next steps:` on, colour stripped. */ + async function nextSteps(argv: string[], userAgent: string): Promise { + vi.stubEnv('npm_config_user_agent', userAgent); + const logs: string[] = []; + const logSpy = vi.spyOn(console, 'log').mockImplementation((...a: unknown[]) => { + logs.push(a.join(' ')); + }); + try { + await Init.run(argv, { root: CLI_ROOT }); + } finally { + logSpy.mockRestore(); + } + const out = logs.join('\n').replace(SGR, ''); + const at = out.indexOf('Next steps:'); + expect(at, `no "Next steps:" block was printed:\n${out}`).toBeGreaterThan(-1); + return out.slice(at); + } + + it('--package-manager pnpm, invoked through npm: the flag wins', async () => { + const steps = await nextSteps(['probe-app', '--no-install', '--package-manager', 'pnpm'], NPM_UA); + expect(steps).toMatch(/^\s*pnpm install\s+# Install dependencies$/m); + expect(steps).toMatch(/^\s*pnpm exec objectstack validate\b/m); + // Word boundary: the text `pnpm install` itself contains the substring `npm install`. + expect(steps).not.toMatch(/\bnpm install\b/); + expect(steps).not.toMatch(/\bnpx objectstack\b/); + }, 60_000); + + it('no flag, invoked through pnpm: the invoking package manager', async () => { + const steps = await nextSteps(['probe-app', '--no-install'], PNPM_UA); + expect(steps).toMatch(/^\s*pnpm install\s+# Install dependencies$/m); + expect(steps).not.toMatch(/\bnpm install\b/); + }, 60_000); + + it('no flag, invoked through npm: still npm (control)', async () => { + const steps = await nextSteps(['probe-app', '--no-install'], NPM_UA); + expect(steps).toMatch(/^\s*npm install\s+# Install dependencies$/m); + expect(steps).toMatch(/^\s*npx objectstack validate\b/m); + }, 60_000); +}); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index ccd65107b09..c4c2caca967 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -1219,7 +1219,14 @@ export default class Init extends Command { let installSucceeded = false; let installAttempted = false; - let chosenPm: 'npm' | 'pnpm' | 'yarn' | 'bun' = 'npm'; + // Resolved before the install branch, not inside it: the Next-steps block + // below names this package manager whether or not an install ran. Resolved + // only under `--install`, a `--no-install` run kept a literal `'npm'` and + // printed `npm install` even for `--package-manager pnpm` or a pnpm-invoked + // run — the hardcoded answer `create-objectstack`'s Next steps are pinned + // against (`scaffold-next-steps-pm.test.ts`). + const chosenPm: 'npm' | 'pnpm' | 'yarn' | 'bun' = + (flags['package-manager'] as 'npm' | 'pnpm' | 'yarn' | 'bun' | undefined) ?? detectPackageManager(); try { // 1. Create package.json if missing @@ -1271,7 +1278,6 @@ export default class Init extends Command { // Install dependencies if (flags.install) { - chosenPm = (flags['package-manager'] as typeof chosenPm | undefined) ?? detectPackageManager(); printStep(`Installing dependencies with ${chosenPm}...`); installAttempted = true; const { execSync } = await import('child_process'); diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 19777ea426c..e371596371c 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -91,7 +91,7 @@ export default class Start extends Command { static override flags = { // Server - port: Flags.integer({ char: 'p', description: 'Port to listen on (overrides $PORT, default 3000)' }), + port: Flags.integer({ char: 'p', description: 'Port to listen on, default 3000 (overrides $OS_PORT; $PORT is the legacy alias)' }), ui: Flags.boolean({ description: 'Mount the Console portal at /_console/ (default: true so you can install marketplace apps)', default: true,