@neocompose/cli

中文文档
0.93.5发布于 2026/11/02MITES Module

Neo Compose native project-source CLI with bidirectional sync.

原始使用文档
文档目录14 节
  1. Source architecture
  2. Run
  3. Format-4 working copy
  4. Organize source locally
  5. Native Neo authoring
  6. Root values and class-owned values
  7. Files
  8. NeoFlow dialogue authoring
  9. Synchronization workflow
  10. NeoScript unit tests
  11. Commands
  12. Regenerating dialogue source names
  13. Packaging
  14. Source formatting

neo — Neo Compose project-source CLI

neo synchronizes a Git-friendly Neo Compose project working copy with the web editor. Working-copy format 4 is Neo-native: .neo defines the project model and authored values, .neoflow defines dialogue flows, and managed image and audio files stay as ordinary binaries.

The current agent authoring guide is in cli/skills/neocompose-cli/SKILL.md. Feature specs in neo-compose-specs/new-features document design intent and edge cases; the released compiler, contract, and tests are the executable source of truth.

Source architecture

The CLI source separates authoring, document projection, and synchronization:

project-source/   parse, lower, and emit .neo/.neoflow plus managed binaries
project-manifest/ semantic bridge between authored source and project documents
project-sync/     diff, merge, conflict, migration, and CAS state handling

The flow is project source -> ProjectSchemaManifest -> project documents -> sync. ProjectSchemaManifest carries an independently versioned serialized contract so source syntax and server documents can evolve without coupling their version numbers. The current workspace format is still format 4; the manifest's numeric contract version is not a second working-copy format. Dependencies point downward: project-source may use both lower layers, while project-sync never imports source-authoring or manifest code.

Run

npm i -g @neocompose/cli       # published package; installs `neo` on PATH
node cli/bin/neo.mjs <command> # repository development

Set up a working copy with explicit IDs in automation:

neo login --profile editor
neo init --project <id> --version <id> --dir neo

Interactive terminals may select the project, version, and directory. Neo does not install, discover, or require .NET. neo doctor validates the format-4 browser-safe compiler, VS Code analysis contract, tracked source, and managed-file capabilities. It also fails on malformed records in the last-pulled document.

Format-4 working copy

neo/
  neo.json                         # formatVersion: 4
  Project.neo                      # project defaults/settings
  Root.neo                         # protected root shape; editable root values
  Classes/{System/, ...}
  Interfaces/{System/, ...}
  Enums/{System/, ...}
  Relations.neo                    # generic project relations, when present
  Templates/
  Localization.neo
  LocalizationStatuses/
  Files/
    Images.neo                     # ImageRegistry Images
    AudioClips.neo                 # AudioClipRegistry AudioClips
    Images/                        # tracked image bytes
    AudioClips/                    # tracked audio bytes
  DialogueGroups/
  Dialogues/*.neoflow
  Migrations/*.neo
  **/*.spec.neo                   # colocated local-only NeoScript tests
  .neo/
    state.json                     # private merge/CAS bases; never hand-edit
    conflicts/
    test-build/                    # local test compilation metadata

Production .neo, .neoflow, and supported managed binaries are tracked. Colocated .spec.neo files are test-only: they never enter status, source identity, pull/reset, or push. .neo/ is ignored private state. There is no schema .csproj, C# authoring tree, Roslyn compiler, generated authoring DLL, per-function sidecar tree, or checked-in dialogue JSON.

Only formatVersion: 4 is accepted. Format 4 is a clean break; it does not read or convert a format-3 C# checkout. Push intended changes with the older CLI first, preserve any local source you need, then reconstruct from the authoritative server:

neo init --project <id> --version <id> --dir neo

For an existing format-4 workspace, neo pull --reset regenerates canonical source and managed binaries. Normal neo pull performs a stable-ID-aware three-way merge, preserves source placement when possible, and uses a durable transaction cursor to fetch only records changed since the previous sync. Undo/reset metadata invalidates that cursor and safely falls back to a full snapshot.

In Unity projects, neo.json may use unityConfigPath instead of storing projectId and versionId. The referenced NeoComposeConfig.asset then owns those IDs and receives branch/version switches. neo export writes into that Unity project; without unityConfigPath, it offers to link the config asset for this project it finds in the Unity project around the working copy.

Validated source lowering, commit preparation, animation validation, and Unity builds live in a shared file cache under .neo/build/. A new CLI process can reuse them; keeping neo dev running is not required. Exact input and dependency checks invalidate affected results. Independent dialogue prose changes preserve structural work, including a pull that commits those changes. Structural source edits still require broader analysis and preparation.

Export checks output bytes and restores missing or modified generated files from its cache, without rewriting unchanged files. neo dev uses the same preparation and export rules after each save.

Pull, push, and export refuse a working copy whose pulled project or version differs from the config's, which happens when the asset changes outside the CLI. Run neo branch switch <versionId> to reset and pull the config's version.

Organize source locally

Move .neo and .neoflow files into any project folder, and group declarations in files as you prefer. Keep existing IDs. Pull, force, and reset read your current placement before applying server state. New server declarations use the default folders. System declarations use a System subfolder, such as Classes/System, Enums/System, or Interfaces/System. Ownership comes from the reserved system identity, so project types keep their normal defaults. Managed images and audio can also move. Update their typed registry paths.

Extract large Root or static initializers into typed declarations without imports:

// Configuration/ProjectSortingLayers.neo
List<NeoSortingLayer> SORTING_LAYERS = [
  new NeoSortingLayer() { Name = "Default" }
];

// Inside the Root Assets initializer
SortingLayers = SORTING_LAYERS

Existing nested @id annotations stay with the extracted initializer. The consuming member keeps its value ID; no ID is required at the use. An aggregate has one owning assignment. Additional references use Reference(NAME) where the member supports references. Independent copies need fresh nested IDs; the editor's duplicate action removes the old IDs. Scalars may have several owners. If web edits make those values differ, pull inlines the differing uses.

The shared language service offers extract-to-file for a selected complete initializer, inline for a sole use, and duplicate. These declarations are local project setup and do not create runtime globals. Use them as complete value initializers; using their names inside executable expressions such as BASE + 1 is rejected. Initializer cycles and ambiguous aggregate ownership fail validation before synchronization. Fix source errors before resetting a checkout with extracted initializers so their layout can be recovered safely.

Migrations can move too. Keep their /// @migration header and /// @order N directive, or retain the numeric filename prefix for ordering.

Native Neo authoring

Classes, interfaces, and enums are top-level declarations. Schema functions, getters, and setters live inline and compile as NeoScript; they do not have separate source files.

@id("enum-item-rarity-id")
enum ItemRarity {
  @id("rarity-common-id")
  Common = "Common text",
}

@id("class-inventory-item-id")
@storage(allowed: .Immutable)
abstract class InventoryItem<
  @id("inventory-context-generic-id")
  TContext extends SomeClass
> {
  @id("item-name-id")
  @settings(searchBy: .MemberKey)
  virtual string Name = "";

  @id("item-rarity-id")
  abstract ItemRarity Rarity;

  @id("item-stack-size-id")
  @settings(min: 1, max: 999)
  int StackSize = 1;

  @id("item-display-name-id")
  string DisplayName {
    get {
      return $"{Name} ({Rarity})";
    }
  }

  @id("item-use-id")
  abstract void Use(TContext context);
}

The source identifier is the persisted name. @settings and @storage are typed by their declaration context; invalid fields and enum cases are diagnostics. Swift-like .EnumCase syntax works whenever the expected enum type is known. Focused annotations such as @id, @visibility, and @relations carry cross-cutting behavior. Never author @system; it is reserved for platform-owned system records. @visibility(.Hidden) marks a class the member selector and classes tree leave out; absent means visible.

Lists take repeatable @index and @column annotations naming a field on the entry class. An index requires a non-localizable string or a single-select enum; a column may also target the reserved __other__ key, which carries the default layout for every field a subclass adds.

@id("outpost-list-id")
@settings(kind: .Unordered)
@index(member: Slug, kind: .Unique)
@column(member: Slug, width: 158, pin: .Leading)
@column(member: Notes, visibility: .Hidden)
public List<Outpost> Outposts = [];

Stable IDs preserve identity across rename, reorder, and file moves. Omit an @id only to create something. A successful real push assigns the ID and rewrites source; dry-run, validation failure, upload failure, and CAS failure leave source byte-for-byte unchanged. Collection item identity uses the same native annotation:

Tags = [
  @id("story-tag-item-id")
  "story",
];

Root values and class-owned values

Root.neo contains the compiler-owned Root root = new() { ... } envelope. Its member names, types, IDs, lock annotations, and storage settings are protected, while the root.Assets, root.Save, root.Session, and root.User value initializers are editable.

Reusable values are stored static members on ordinary project classes. There is no ValueRegistry or generated Values namespace.

@id("assets-class-id")
@storage(allowed: .Immutable)
class Assets {
  @id("capitol-member-id")
  static Outpost Capitol = new {
    Name = "Capitol",
    Image = Images.Capitol.Slice(0),
  };

  @id("home-getter-id")
  static Outpost Home {
    get {
      return Assets.Capitol;
    }
  }
}

The member ID anchors the stored binding; do not add a second ID to the static initializer. Nested class and collection rows remain independently identified.

Defaults materialize once when a value is created. Existing instances do not stay linked to later class-default changes. Logical lookup/dialogue references use the native typed intrinsic. A reference to a value type, such as Reference<int>, names a member path:

Reference(Assets.Capitol)
Reference(root.Assets.Cosmetics.Pants)
Reference(root.Player.Gold)
Reference<PantsAsset>(key: "pants.long")
Reference<NeoDialogue>(id: "dialogue-id")

Project.neo, typed texture/audio templates, localization configuration and statuses, priority groups/options, dialogue groups, and direct relations use the same top-level typed declaration grammar. The compiler derives structural row identities, storage placement, generic stamps, localized-text links, and other persistence details that are not authorable source.

Files

Images and audio are declared in typed registries and referenced directly:

ImageRegistry Images = new() {
  @id("sword-image-file-id")
  @settings(template: PixelArt)
  NeoImage Sword = new("Files/Images/Sword.png");
}

AudioClipRegistry AudioClips = new() {
  @id("sword-hit-audio-file-id")
  @settings(template: SoundEffect)
  NeoAudioClip SwordHit = new("Files/AudioClips/SwordHit.wav");
}

Use Images.Sword.Slice(0) for a sprite and AudioClips.SwordHit for an audio member. Dropping a supported binary into Files/Images/ or Files/AudioClips/ creates a provisional symbol; status/diff report it, and a successful push writes the explicit declaration and assigned ID. neo files add can create that pending declaration before push.

Pull and push compare server-verified SHA-256 digests. Divergent local/remote bytes keep the local file and place the verified remote side under .neo/conflicts/files/<file-id>/ until explicitly resolved.

NeoFlow dialogue authoring

Each .neoflow file contains exactly one sealed, non-generic class directly derived from the system NeoDialogue type and exactly one required trigger override. Dialogue functions and lexical value bindings are ordinary typed Neo code. Main-locale prose is inline; other locales and workflow metadata remain in localized-text records.

Graph constructors use NeoFlow-only trailing bodies. => Node and a final return Node; are equivalent destinations; there is no authorable to: argument. Terminal text/actions nodes may omit a destination, while triggers, options, and outcomes require one.

@id("capitol-dialogue-id")
@settings(name: "Capitol: cold boot", saveOptionChoices: true)
sealed class CapitolColdBoot : NeoDialogue {
  @primary Outpost capitol = Assets.Capitol;

  @id("can-start-function-id")
  bool CanStart() {
    return capitol.Name == "Capitol";
  }

  @id("capitol-trigger-id")
  override Trigger Trigger = new(
    group: CapitolDialogues.High,
    when: [
      @id("can-start-condition-use-id")
      CanStart,
    ]
  ) => Welcome;

  @id("welcome-node-id")
  Text Welcome = new(name: "Welcome!") {
    """
    Welcome to {capitol.Name}.
    """

    @id("continue-option-id")
    Option Continue = new() {
      """
      Tell me more.
      """
      return Finish;
    }
  }

  @id("finish-node-id")
  Text Finish = new() {
    """
    Until next time.
    """
  }
}

Persisted condition uses, action invocations, mutations, pauses, options, and outcomes each have their own stable @id. References made through lexical bindings and transitive function calls derive the same linked values as the web editor. Run neo dialogue dryrun <ref> after dialogue edits to traverse option paths against a fresh save and catch runtime storage-ownership errors.

Synchronization workflow

neo pull
# edit .neo, .neoflow, and managed binaries
neo status
neo diff
neo dialogue dryrun <ref>
neo push --dry-run
neo push

A pull followed immediately by status or push dry-run must be a semantic no-op. If it reports authoring changes, stop and report a round-trip bug rather than pushing.

Pull merges source concepts and collections by stable identity, never by index. Conflicts are explicit and block compilation. Edit source to the desired result, or use neo resolve --mine|--theirs as a whole-side convenience. There is no force-CAS bypass. Accept a required compatibility bump only after reviewing it with neo push --accept-bump.

One push prepares its commit locally: it recompiles changed NeoScript and schema-affected callers, materializes values, and validates the whole graph against the pulled head. The server checks that head, each change's CAS base, authorization, and row shapes, then atomically commits semantic record changes, localized main-locale edits, and staged files. It accepts preparation only from a CLI on its own NeoScript compiler revision. neo push --dry-run runs the same preparation plus the complete source-to-record rehearsal.

Create classes, stored values, file declarations, and dialogues by editing their tracked .neo or .neoflow source directly. Add managed binaries under Files/Images/ or Files/AudioClips/; neo push discovers and stages them with the source transaction. The CLI intentionally has no parallel scaffold or low-level content-write commands.

NeoScript unit tests

Place *.spec.neo files anywhere beside project source. They see the complete locally prepared production graph—including unpushed declarations, values, and function bodies—plus a test-only typed prelude. Production source never sees test globals.

describe("Player.TakeDamage", () => {
  test("rejects negative damage", () => {
    var player = new Player();
    expect(() => { player.TakeDamage(-1); }).toThrow("negative");
  });
});

Run all tests, narrow by file/directory, or filter the full test name with a JavaScript regular expression:

neo test
neo test Classes/Player.spec.neo
neo test -t "TakeDamage"
neo test --seed=17
neo test --maxWorkers=4
neo test --runInBand
neo test --reporter=json --outputFile=.neo/test-results.json

--seed accepts a signed safe decimal integer, reduced modulo 2^32. The JSON report records the effective seed. Without it, the project fingerprint selects the seed. Registration and each isolated test start fresh; beforeEach, body, and afterEach share the test's stream. Worker counts and filters do not change that starting stream. neo script eval --seed 17 seeds a single evaluation of the server snapshot.

Local candidate evaluation reserves pending identities in .neo/export.json so random stored constructions agree across test, dry-run, export, and push. An identity-only file does not mark a Unity export successful. Cached candidates restore missing reservations and rebuild on conflicts. Push hooks consume the parent's prepared candidate; incomplete hook metadata fails explicitly.

Spec files run in parallel with up to four available CPU workers by default. Set test.maxWorkers in neo.json or override it with --maxWorkers <positive integer>. --runInBand overrides both and runs serially. A single selected file runs in the main process. Hooks and tests within each file remain ordered; fixtures and mocks stay isolated. Reports keep file and test registration order.

The JSON reporter uses schemaVersion: 2. Failure frames include a member name and optional file, line, and column. Frames without a file include coordinateSpace to distinguish body-relative locations; unknown positions are omitted. Consumers upgrading from version 1 must allow these optional locations.

The JSON output file is written atomically. Reports distinguish elapsed startup and execution from summed test work, which can exceed elapsed time in parallel runs. JSON timings.workerStartupMs measures worker initialization and timings.executeMs measures elapsed execution.

Tests compile and validate the local candidate once. They omit the extra source serialization and re-lowering rehearsal used by neo push --dry-run; production syntax, type checks, and whole-graph validation remain required. Spec compilation shares one immutable compiler context and schema index across cache misses.

Test compilation metadata and the experimental verified prepared-project cache live in .neo/test-build/; neither they nor test files are server records. Production source, managed files, configuration, workspace state, CLI version, and compiler revision invalidate the prepared project, while editing only a .spec.neo file reuses it. Missing, corrupt, or checksum-mismatched cache entries are rebuilt. A workspace must have been initialized or pulled once so .neo/state.json contains the offline project facts, but running tests performs no login or network request.

Native functions cannot execute in the local evaluator and therefore fail closed unless mocked. Use mock.native(Member), mock.function(Member), or spyOn(Member), then configure returns, returnsOnce, throws, throwsOnce, doesNothing, or implementation. Mocks and call history are isolated per test.

To gate pushes, add either or both commands to neo.json:

{
  "prePushHook": "neo test",
  "prePushDryRunHook": "neo test --reporter=json --outputFile=.neo/test-results.json"
}

The real-push hook is intentionally not reused for dry runs: arbitrary hook commands may have side effects. Each applicable hook runs only after local candidate validation and before authentication/network activity; a non-zero exit aborts. neo push --no-verify is an explicit local bypass, not a server authorization boundary.

Commands

CommandPurpose
--versionPrint the installed CLI package version.
login [--api <url>]Authenticate the selected profile.
whoami [--api <url>]Inspect the selected profile.
logout [--api <url>]Delete the stored credential for one API origin and revoke its session.
init --project <id> [--version <id>]Create a format-4 working copy and perform its first reset pull.
doctorValidate format/compiler/editor/source/file contracts; no .NET discovery.
pull [--force|--reset] [--regenerate-source-names]Merge remote changes or regenerate canonical source and binaries.
status [--json]Compile source and summarize semantic records and upload intent.
diff [--json]Show semantic records, spans, digests, and upload intent.
push [--dry-run] [--accept-bump] [--no-verify]Validate or commit one atomic compare-and-swap transaction.
test [file-or-dir ...] [-t pattern]Compile the unpushed local candidate and run *.spec.neo tests.
dev [--unity-project <dir>]Run neo export after every local change; never pulls or pushes.
resolve --mine|--theirsResolve all current source/binary conflicts with one side.
script check|compile|evalCompile or preview NeoScript with project context; never commit writes.
dialogue dryrun <ref>Simulate a locally authored dialogue candidate.
locInspect or edit localization data not represented in main-locale source.
migrate new|list|check|run|pruneAuthor and manage tracked NeoScript migrations.
branchManage copy-on-write branches.
mergeMerge branch changes.
releaseCut, publish, archive, or restore releases.
channelManage release channels.
historyInspect and deliberately rewrite retained history.
export [--unity-project <dir>]Write the working copy, local edits included, into a Unity project.

With the deliberate exception of neo loc, content authoring goes through tracked native source and neo push. Localization commands can mutate authoritative localization records that .neo does not fully represent; run neo pull immediately afterward before further source edits or push.

login, whoami, and logout use the current workspace's apiBaseUrl when run beneath a neo.json. An explicit --api takes priority. Outside a workspace, they use the CLI build's default API origin. Login prints the selected origin before it requests or stores a credential.

Tokens use the OS credential store when available and a protected local file only as fallback. CI can use NEO_COMPOSE_TOKEN or --token-stdin. The editor profile cannot publish releases; server authorization remains the security boundary.

Regenerating dialogue source names

Every pull, including --force and --reset, keeps the dialogue source names the server stores. To replace generated defaults such as Value_1a2b3c4d5e6f, NeoFunction_…, or Conditions1 with names derived from the editor, run:

neo pull --regenerate-source-names
neo status
neo push --dry-run

Names you chose are kept. When a binding is renamed, code that refers to it is rewritten to match. Custom linked values prefer a non-empty string member named Name, using PascalCase at dialogue scope and camelCase inside nodes.

The pull leaves the regenerated names as local changes. IDs are unchanged, and nothing is written to the server until neo push succeeds.

Packaging

node cli/build.mjs bundles the Node orchestrator and browser-safe Neo language/compiler/editor assets. The npm package contains no C# SDK, Roslyn application, DLL, or .NET runtime metadata. Package checks reject those artifacts and verify that the shared compiler assets and skill are present.

Useful checks while developing:

npm run typecheck
npx vitest run cli
npm --prefix cli run test:package

Run the repository's full verification suite before release, package and exercise the VS Code extension, verify the Hello World and live neowyn format-4 no-op workflows, and run npm run doctor last.

Source formatting

The CLI and NeoScript VS Code extension use the same formatter. Put project preferences in .editorconfig, including in a parent directory of neo/:

[*.{neo,neoflow}]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true

Nested EditorConfig sections override parent sections. Without an explicit convention, CLI regeneration uses two spaces, LF, and a final newline. The VS Code extension uses your editor settings as the fallback and disables indentation guessing by default. To use four spaces or tabs across sync and editor saves, set that preference in .editorconfig.

Push preserves authored text when the emitted tokens are unchanged, including ordinary comments and blank lines. Pull preserves files without semantic changes. Formatting-only body and source-location differences do not create a later push, even alongside other edits. Local tests use current file locations; neo push --force-recompile explicitly refreshes stored locations. Changes inside string literals remain significant. Files that must be regenerated use the shared formatting policy. Web editors use the shared two-space default and keep wrapper indentation out of stored bodies.

版本与发布记录

@neocompose/cli 共发布 346 个版本,首次发布于 2026/06/12;过去 12 个月发布了 346 个版本。

版本发布日期距今
0.93.5latest2026/11/021 天前
0.93.42026/11/011 天前
0.93.32026/11/012 天前
0.93.22026/10/313 天前
0.93.12026/10/313 天前
0.93.02026/10/313 天前

关于 @neocompose/cli 的常见问题

@neocompose/cli 是什么?

neocompose原生项目源CLI与双向同步。

如何安装 @neocompose/cli?

在项目目录运行 npm install @neocompose/cli,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 neo,可以用 npx neo 直接运行。

@neocompose/cli 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/neocompose__cli 是否存在。

@neocompose/cli 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@neocompose/cli 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@neocompose/cli 可以商用吗?

@neocompose/cli 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@neocompose/cli 还在维护吗?

最新版本 0.93.5 发布于 2026/11/02(1 天前);至今共发布 346 个版本,其中过去 12 个月发布了 346 个;首次发布于 2026/06/12。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。