@neocompose/cli 0.3.0 → 0.3.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1] - 2026-07-16
4
+
5
+ ### Added
6
+
7
+ - Added `neo history inspect`, `undo`, `revert`, and `flatten` workflows with
8
+ interactive project and version selection.
9
+ - Added deterministic `@current` and `@latest` version resolution for history
10
+ commands.
11
+
12
+ ### Changed
13
+
14
+ - Reduced project synchronization I/O with incremental export manifests and
15
+ bounded snapshot retrieval.
16
+
3
17
  ## [0.3.0] - 2026-07-15
4
18
 
5
19
  ### Breaking
package/dist/neo.mjs CHANGED
@@ -1390,7 +1390,7 @@ var init_source_protocol = __esm({
1390
1390
  SCHEMA_SOURCE_MANIFEST_NAME = "neo-schema-manifest-v3";
1391
1391
  SCHEMA_COMPILER_PROTOCOL_VERSION = 4;
1392
1392
  SCHEMA_SDK_VERSION = "3.0.0";
1393
- SCHEMA_CLI_VERSION = "0.3.0";
1393
+ SCHEMA_CLI_VERSION = "0.3.1";
1394
1394
  SCHEMA_API_TARGET = "netstandard2.1";
1395
1395
  SCHEMA_REFERENCE_ASSEMBLY_SET = "NETStandard.Library.Ref/2.1.0";
1396
1396
  SCHEMA_REQUIRED_TOOLING_FILES = [
@@ -31121,10 +31121,15 @@ function appendBucketRecord(buckets, recordKind, data) {
31121
31121
  }
31122
31122
  buckets[bucket].push(data);
31123
31123
  }
31124
- function readProjectDocument(value) {
31124
+ function readProjectDocument(value, options = {}) {
31125
31125
  if (!isObject2(value)) {
31126
31126
  throw new Error("Convex project document query returned a non-object.");
31127
31127
  }
31128
+ const localizationConfig = options.localizationConfig === "nullable" ? readOptionalNullableField(
31129
+ value,
31130
+ "localizationConfig",
31131
+ isProjectLocalizationConfig
31132
+ ) : readField(value, "localizationConfig", isProjectLocalizationConfig);
31128
31133
  return {
31129
31134
  version: readField(value, "version", isProjectVersion),
31130
31135
  versions: readArrayField(value, "versions", isProjectVersion),
@@ -31176,11 +31181,7 @@ function readProjectDocument(value) {
31176
31181
  "audioClipTemplates",
31177
31182
  isUnityAudioClipImportSettingsTemplate
31178
31183
  ),
31179
- localizationConfig: readField(
31180
- value,
31181
- "localizationConfig",
31182
- isProjectLocalizationConfig
31183
- ),
31184
+ localizationConfig,
31184
31185
  localizationStatuses: readArrayField(
31185
31186
  value,
31186
31187
  "localizationStatuses",
@@ -31221,6 +31222,14 @@ function readField(source, key, isValid) {
31221
31222
  `Convex project document field "${key}" has an invalid shape.`
31222
31223
  );
31223
31224
  }
31225
+ function readOptionalNullableField(source, key, isValid) {
31226
+ const value = source[key];
31227
+ if (value === void 0 || value === null) return null;
31228
+ if (isValid(value)) return value;
31229
+ throw new Error(
31230
+ `Convex project document field "${key}" has an invalid shape.`
31231
+ );
31232
+ }
31224
31233
  function readArrayField(source, key, isValid) {
31225
31234
  const value = source[key];
31226
31235
  if (!Array.isArray(value)) {
@@ -32162,6 +32171,565 @@ var init_init = __esm({
32162
32171
  }
32163
32172
  });
32164
32173
 
32174
+ // src/commands/history-selection.ts
32175
+ async function resolveHistoryScope(options) {
32176
+ const projectSelector = await readHistorySelector({
32177
+ command: options.command,
32178
+ kind: "project",
32179
+ provided: options.projectSelector
32180
+ });
32181
+ const project = await options.resolveProject(projectSelector);
32182
+ console.log(`${sym.info} Project: ${project.name} (${project.id})`);
32183
+ if (!options.requiresVersion) {
32184
+ return { project, version: null };
32185
+ }
32186
+ const versionSelector = await readHistorySelector({
32187
+ command: options.command,
32188
+ kind: "version",
32189
+ provided: options.versionSelector
32190
+ });
32191
+ const version = await options.resolveVersion(project.id, versionSelector);
32192
+ console.log(`${sym.info} Version: ${version.name} (${version.id})`);
32193
+ return { project, version };
32194
+ }
32195
+ async function readHistorySelector(args) {
32196
+ if (args.provided !== null) return normalizeHistorySelector(args.provided);
32197
+ const flag = args.kind === "project" ? "--project" : "--version";
32198
+ if (!isInteractive()) {
32199
+ throw new Error(
32200
+ `neo history ${args.command} requires ${flag} <id|@latest|@current> outside an interactive terminal.`
32201
+ );
32202
+ }
32203
+ const label = args.kind === "project" ? "Project" : "Version";
32204
+ const value = await promptInput({
32205
+ message: `${label} ID (@latest or @current; Enter for latest)`,
32206
+ nonInteractiveHint: `Pass ${flag} <id|@latest|@current>.`
32207
+ });
32208
+ return normalizeHistorySelector(value);
32209
+ }
32210
+ function normalizeHistorySelector(value) {
32211
+ const trimmed = value.trim();
32212
+ if (trimmed.length === 0 || trimmed === "@latest" || trimmed === "@current") {
32213
+ return "@latest";
32214
+ }
32215
+ if (trimmed.startsWith("@")) {
32216
+ throw new Error(
32217
+ `Unknown history selector "${trimmed}". Use a stable ID, @latest, or @current.`
32218
+ );
32219
+ }
32220
+ return trimmed;
32221
+ }
32222
+ var init_history_selection = __esm({
32223
+ "src/commands/history-selection.ts"() {
32224
+ "use strict";
32225
+ init_ui();
32226
+ }
32227
+ });
32228
+
32229
+ // src/commands/history-inspect.ts
32230
+ function inspectHistoryDataset(dataset) {
32231
+ const brokenReferences = [];
32232
+ const projectSnapshots = new Map(
32233
+ dataset["project-snapshots"].map((snapshot) => [snapshot.id, snapshot])
32234
+ );
32235
+ const projectTransactions = new Set(
32236
+ dataset["project-transactions"].map((transaction) => transaction.id)
32237
+ );
32238
+ const reachableProjectSnapshots = walkAncestors({
32239
+ roots: dataset["project-heads"].map((head) => ({
32240
+ id: head.snapshotId,
32241
+ source: `project head "${head.id}"`
32242
+ })),
32243
+ rows: projectSnapshots,
32244
+ baseId: (snapshot) => snapshot.baseSnapshotId,
32245
+ kind: "project snapshot",
32246
+ brokenReferences
32247
+ });
32248
+ for (const snapshot of projectSnapshots.values()) {
32249
+ if (!projectTransactions.has(snapshot.transactionId)) {
32250
+ brokenReferences.push(
32251
+ `Project snapshot "${snapshot.id}" references missing transaction "${snapshot.transactionId}".`
32252
+ );
32253
+ }
32254
+ }
32255
+ const reachableProjectTransactions = new Set(
32256
+ [...reachableProjectSnapshots].map((id) => projectSnapshots.get(id)?.transactionId).filter(
32257
+ (id) => id !== void 0 && projectTransactions.has(id)
32258
+ )
32259
+ );
32260
+ const wikiSnapshots = new Map(
32261
+ dataset["wiki-snapshots"].map((snapshot) => [snapshot.id, snapshot])
32262
+ );
32263
+ const wikiTransactions = new Set(
32264
+ dataset["wiki-transactions"].map((transaction) => transaction.id)
32265
+ );
32266
+ const wikiDocuments = new Map(
32267
+ dataset["wiki-documents"].map((document) => [document.id, document])
32268
+ );
32269
+ const reachableWikiSnapshots = walkAncestors({
32270
+ roots: dataset["wiki-heads"].map((head) => ({
32271
+ id: head.snapshotId,
32272
+ source: `wiki head "${head.id}"`
32273
+ })),
32274
+ rows: wikiSnapshots,
32275
+ baseId: (snapshot) => snapshot.baseSnapshotId,
32276
+ kind: "wiki snapshot",
32277
+ brokenReferences
32278
+ });
32279
+ const reachableWikiDocuments = walkAncestors({
32280
+ roots: [...reachableWikiSnapshots].flatMap((snapshotId) => {
32281
+ const snapshot = wikiSnapshots.get(snapshotId);
32282
+ return snapshot === void 0 ? [] : [
32283
+ {
32284
+ id: snapshot.yjsDocumentId,
32285
+ source: `wiki snapshot "${snapshot.id}"`
32286
+ }
32287
+ ];
32288
+ }),
32289
+ rows: wikiDocuments,
32290
+ baseId: (document) => document.forkedFromDocumentId,
32291
+ kind: "wiki document",
32292
+ brokenReferences
32293
+ });
32294
+ const reachableWikiTransactions = /* @__PURE__ */ new Set();
32295
+ for (const snapshotId of reachableWikiSnapshots) {
32296
+ const snapshot = wikiSnapshots.get(snapshotId);
32297
+ if (snapshot !== void 0 && wikiTransactions.has(snapshot.transactionId)) {
32298
+ reachableWikiTransactions.add(snapshot.transactionId);
32299
+ }
32300
+ }
32301
+ for (const snapshot of wikiSnapshots.values()) {
32302
+ if (!wikiTransactions.has(snapshot.transactionId)) {
32303
+ brokenReferences.push(
32304
+ `Wiki snapshot "${snapshot.id}" references missing transaction "${snapshot.transactionId}".`
32305
+ );
32306
+ }
32307
+ if (!wikiDocuments.has(snapshot.yjsDocumentId)) {
32308
+ brokenReferences.push(
32309
+ `Wiki snapshot "${snapshot.id}" references missing document "${snapshot.yjsDocumentId}".`
32310
+ );
32311
+ }
32312
+ }
32313
+ let unreachableWikiRevisions = 0;
32314
+ for (const revision of dataset["wiki-revisions"]) {
32315
+ const documentExists = wikiDocuments.has(revision.yjsDocumentId);
32316
+ const transactionExists = wikiTransactions.has(revision.transactionId);
32317
+ if (!documentExists) {
32318
+ brokenReferences.push(
32319
+ `Wiki revision "${revision.id}" references missing document "${revision.yjsDocumentId}".`
32320
+ );
32321
+ }
32322
+ if (!transactionExists) {
32323
+ brokenReferences.push(
32324
+ `Wiki revision "${revision.id}" references missing transaction "${revision.transactionId}".`
32325
+ );
32326
+ }
32327
+ if (transactionExists && reachableWikiDocuments.has(revision.yjsDocumentId)) {
32328
+ reachableWikiTransactions.add(revision.transactionId);
32329
+ } else {
32330
+ unreachableWikiRevisions += 1;
32331
+ }
32332
+ }
32333
+ return {
32334
+ versions: {
32335
+ total: dataset.versions.length,
32336
+ archived: dataset.versions.filter((version) => version.archived).length
32337
+ },
32338
+ projectRecords: {
32339
+ heads: dataset["project-heads"].length,
32340
+ snapshots: projectSnapshots.size,
32341
+ transactions: projectTransactions.size,
32342
+ unreachableSnapshots: projectSnapshots.size - reachableProjectSnapshots.size,
32343
+ unreachableTransactions: projectTransactions.size - reachableProjectTransactions.size,
32344
+ duplicateSemanticSnapshots: duplicateCount(
32345
+ dataset["project-snapshots"],
32346
+ (snapshot) => `${snapshot.recordKind}:${snapshot.recordId}:${snapshot.contentHash}`
32347
+ )
32348
+ },
32349
+ wiki: {
32350
+ heads: dataset["wiki-heads"].length,
32351
+ snapshots: wikiSnapshots.size,
32352
+ transactions: wikiTransactions.size,
32353
+ documents: wikiDocuments.size,
32354
+ revisions: dataset["wiki-revisions"].length,
32355
+ unreachableSnapshots: wikiSnapshots.size - reachableWikiSnapshots.size,
32356
+ unreachableTransactions: wikiTransactions.size - reachableWikiTransactions.size,
32357
+ unreachableDocuments: wikiDocuments.size - reachableWikiDocuments.size,
32358
+ unreachableRevisions: unreachableWikiRevisions,
32359
+ duplicateSemanticSnapshots: duplicateCount(
32360
+ dataset["wiki-snapshots"],
32361
+ (snapshot) => `${snapshot.pageId}:${snapshot.contentHash}`
32362
+ ),
32363
+ duplicateSemanticDocuments: duplicateCount(
32364
+ dataset["wiki-documents"],
32365
+ (document) => `${document.pageId}:${document.contentHash}`
32366
+ )
32367
+ },
32368
+ brokenReferences
32369
+ };
32370
+ }
32371
+ function walkAncestors(args) {
32372
+ const reachable = /* @__PURE__ */ new Set();
32373
+ const stack = [...args.roots];
32374
+ while (stack.length > 0) {
32375
+ const current = stack.pop();
32376
+ if (reachable.has(current.id)) continue;
32377
+ const row = args.rows.get(current.id);
32378
+ if (row === void 0) {
32379
+ args.brokenReferences.push(
32380
+ `${current.source} references missing ${args.kind} "${current.id}".`
32381
+ );
32382
+ continue;
32383
+ }
32384
+ reachable.add(current.id);
32385
+ const baseId = args.baseId(row);
32386
+ if (baseId !== null) {
32387
+ stack.push({
32388
+ id: baseId,
32389
+ source: `${args.kind} "${current.id}"`
32390
+ });
32391
+ }
32392
+ }
32393
+ return reachable;
32394
+ }
32395
+ function duplicateCount(rows, key) {
32396
+ const counts = /* @__PURE__ */ new Map();
32397
+ for (const row of rows) {
32398
+ const value = key(row);
32399
+ counts.set(value, (counts.get(value) ?? 0) + 1);
32400
+ }
32401
+ let duplicates = 0;
32402
+ for (const count of counts.values()) {
32403
+ if (count > 1) duplicates += count - 1;
32404
+ }
32405
+ return duplicates;
32406
+ }
32407
+ var HISTORY_INSPECT_PHASES;
32408
+ var init_history_inspect = __esm({
32409
+ "src/commands/history-inspect.ts"() {
32410
+ "use strict";
32411
+ HISTORY_INSPECT_PHASES = [
32412
+ "versions",
32413
+ "project-heads",
32414
+ "project-snapshots",
32415
+ "project-transactions",
32416
+ "wiki-heads",
32417
+ "wiki-snapshots",
32418
+ "wiki-transactions",
32419
+ "wiki-documents",
32420
+ "wiki-revisions"
32421
+ ];
32422
+ }
32423
+ });
32424
+
32425
+ // src/commands/history.ts
32426
+ var history_exports = {};
32427
+ __export(history_exports, {
32428
+ resolveHistoryScopeFromServer: () => resolveHistoryScopeFromServer,
32429
+ runHistory: () => runHistory
32430
+ });
32431
+ async function resolveHistoryScopeFromServer(options) {
32432
+ const convex = await createConvexClientForApi(options.apiBaseUrl);
32433
+ return await resolveHistoryScope({
32434
+ command: options.command,
32435
+ projectSelector: options.projectSelector,
32436
+ versionSelector: options.versionSelector,
32437
+ requiresVersion: options.command === "log" || options.command === "flatten",
32438
+ resolveProject: async (selector) => await convex.query(api2.projectHistory.resolveProjectSelector, {
32439
+ selector
32440
+ }),
32441
+ resolveVersion: async (projectId, selector) => await convex.query(api2.projectHistory.resolveVersionSelector, {
32442
+ projectId,
32443
+ selector
32444
+ })
32445
+ });
32446
+ }
32447
+ async function runHistory(options) {
32448
+ assertHistoryOptions(options);
32449
+ const convex = await createConvexClientForApi(options.apiBaseUrl);
32450
+ if (options.command === "inspect") {
32451
+ const scope = await resolveScopeWithClient(convex, options);
32452
+ await printHistoryInspection(convex, scope.project.id);
32453
+ return;
32454
+ }
32455
+ if (options.command === "log") {
32456
+ const scope = await resolveScopeWithClient(convex, options);
32457
+ await printVersionLog(convex, scope);
32458
+ return;
32459
+ }
32460
+ if (options.command === "flatten") {
32461
+ const scope = await resolveScopeWithClient(convex, options);
32462
+ if (scope.version === null) {
32463
+ throw new Error("Flatten requires a resolved version.");
32464
+ }
32465
+ const started = await convex.mutation(api2.projectHistory.planFlatten, {
32466
+ projectId: scope.project.id,
32467
+ versionId: scope.version.id
32468
+ });
32469
+ console.log(`${sym.info} Plan: ${started.planId}`);
32470
+ console.log(`${sym.info} Planning run: ${started.runId}`);
32471
+ const plan = await waitForPlan(convex, started.planId);
32472
+ printPlan(plan);
32473
+ return;
32474
+ }
32475
+ if (options.command === "prune") {
32476
+ const scope = await resolveScopeWithClient(convex, options);
32477
+ const started = await convex.mutation(api2.projectHistory.planPrune, {
32478
+ projectId: scope.project.id
32479
+ });
32480
+ console.log(`${sym.info} Plan: ${started.planId}`);
32481
+ console.log(`${sym.info} Planning run: ${started.runId}`);
32482
+ const plan = await waitForPlan(convex, started.planId);
32483
+ printPlan(plan);
32484
+ return;
32485
+ }
32486
+ if (options.command === "apply") {
32487
+ const plan = await convex.query(api2.projectHistory.getPlan, {
32488
+ planId: options.planId
32489
+ });
32490
+ const expectedScope = plan.versionId === null ? plan.projectId : `${plan.projectId}/${plan.versionId}`;
32491
+ let confirmScope = options.confirmScope;
32492
+ if (plan.productionConfirmationRequired && confirmScope === null) {
32493
+ if (!isInteractive()) {
32494
+ throw new Error(
32495
+ `Production history apply requires --confirm-scope ${expectedScope}.`
32496
+ );
32497
+ }
32498
+ confirmScope = await promptInput({
32499
+ message: `Type ${expectedScope} to confirm production history rewrite`,
32500
+ nonInteractiveHint: `Pass --confirm-scope ${expectedScope}.`,
32501
+ validate: (value) => value === expectedScope ? true : `Confirmation must exactly match ${expectedScope}.`
32502
+ });
32503
+ }
32504
+ const started = await convex.mutation(api2.projectHistory.applyPlan, {
32505
+ planId: plan.id,
32506
+ confirmScope
32507
+ });
32508
+ console.log(`${sym.info} Run: ${started.runId}`);
32509
+ console.log(`${sym.info} State: ${started.state}`);
32510
+ return;
32511
+ }
32512
+ if (options.command === "status") {
32513
+ const status = await convex.query(api2.projectHistory.getRunStatus, {
32514
+ runId: options.runId
32515
+ });
32516
+ printRunStatus(status);
32517
+ return;
32518
+ }
32519
+ if (options.command === "resume") {
32520
+ const resumed = await convex.mutation(api2.projectHistory.resumeRun, {
32521
+ runId: options.runId
32522
+ });
32523
+ console.log(`${sym.info} Run: ${resumed.runId}`);
32524
+ console.log(`${sym.info} State: ${resumed.state}`);
32525
+ return;
32526
+ }
32527
+ }
32528
+ async function resolveScopeWithClient(convex, options) {
32529
+ if (options.command !== "inspect" && options.command !== "log" && options.command !== "prune" && options.command !== "flatten") {
32530
+ throw new Error(`neo history ${options.command} has immutable scope.`);
32531
+ }
32532
+ return await resolveHistoryScope({
32533
+ command: options.command,
32534
+ projectSelector: options.projectSelector,
32535
+ versionSelector: options.versionSelector,
32536
+ requiresVersion: options.command === "log" || options.command === "flatten",
32537
+ resolveProject: async (selector) => await convex.query(api2.projectHistory.resolveProjectSelector, {
32538
+ selector
32539
+ }),
32540
+ resolveVersion: async (projectId, selector) => await convex.query(api2.projectHistory.resolveVersionSelector, {
32541
+ projectId,
32542
+ selector
32543
+ })
32544
+ });
32545
+ }
32546
+ async function waitForPlan(convex, planId) {
32547
+ let lastState = null;
32548
+ while (true) {
32549
+ const plan = await convex.query(api2.projectHistory.getPlan, { planId });
32550
+ if (plan.state !== lastState) {
32551
+ console.log(`${sym.info} Plan state: ${plan.state}`);
32552
+ lastState = plan.state;
32553
+ }
32554
+ if (plan.state === "sealed") return plan;
32555
+ if (plan.state === "failed") {
32556
+ throw new Error(
32557
+ `History plan "${plan.id}" failed: ${plan.error ?? "unknown error"}`
32558
+ );
32559
+ }
32560
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
32561
+ }
32562
+ }
32563
+ async function printVersionLog(convex, scope) {
32564
+ if (scope.version === null) {
32565
+ throw new Error("History log requires a resolved version.");
32566
+ }
32567
+ let cursor = null;
32568
+ let count = 0;
32569
+ while (true) {
32570
+ const page = await convex.query(
32571
+ api2.projectVersionHistory.getChangelog,
32572
+ {
32573
+ projectId: scope.project.id,
32574
+ versionId: scope.version.id,
32575
+ paginationOpts: { cursor, numItems: 50 }
32576
+ }
32577
+ );
32578
+ if (page === null) {
32579
+ throw new Error(
32580
+ `Version "${scope.version.id}" no longer exists in project "${scope.project.id}".`
32581
+ );
32582
+ }
32583
+ for (const transaction of page.transactions) {
32584
+ count += 1;
32585
+ console.log(
32586
+ `${transaction.id} ${new Date(transaction.createdAt).toISOString()} ${transaction.operation} ${transaction.summary ?? ""}`.trimEnd()
32587
+ );
32588
+ }
32589
+ if (page.isDone) break;
32590
+ cursor = page.continueCursor;
32591
+ }
32592
+ if (count === 0) console.log("No retained history entries.");
32593
+ }
32594
+ async function printHistoryInspection(convex, projectId) {
32595
+ const dataset = {
32596
+ versions: [],
32597
+ "project-heads": [],
32598
+ "project-snapshots": [],
32599
+ "project-transactions": [],
32600
+ "wiki-heads": [],
32601
+ "wiki-snapshots": [],
32602
+ "wiki-transactions": [],
32603
+ "wiki-documents": [],
32604
+ "wiki-revisions": []
32605
+ };
32606
+ for (const phase of HISTORY_INSPECT_PHASES) {
32607
+ let cursor = null;
32608
+ while (true) {
32609
+ const page = await convex.query(api2.projectHistory.inspectPage, {
32610
+ projectId,
32611
+ phase,
32612
+ paginationOpts: { cursor, numItems: 100 }
32613
+ });
32614
+ if (page.phase !== phase) {
32615
+ throw new Error(
32616
+ `History inspection requested phase "${phase}" but received "${page.phase}".`
32617
+ );
32618
+ }
32619
+ dataset[phase].push(...page.rows);
32620
+ if (page.isDone) break;
32621
+ cursor = page.continueCursor;
32622
+ }
32623
+ }
32624
+ const report = inspectHistoryDataset(dataset);
32625
+ console.log(
32626
+ `${sym.info} Versions: ${report.versions.total} total, ${report.versions.archived} archived`
32627
+ );
32628
+ console.log(
32629
+ `${sym.info} Project records: ${report.projectRecords.heads} heads, ${report.projectRecords.snapshots} snapshots, ${report.projectRecords.transactions} transactions`
32630
+ );
32631
+ console.log(
32632
+ `${sym.info} Project cleanup candidates: ${report.projectRecords.unreachableSnapshots} unreachable snapshots, ${report.projectRecords.unreachableTransactions} unreachable transactions, ${report.projectRecords.duplicateSemanticSnapshots} duplicate semantic snapshots`
32633
+ );
32634
+ console.log(
32635
+ `${sym.info} Wiki: ${report.wiki.heads} heads, ${report.wiki.snapshots} snapshots, ${report.wiki.transactions} transactions, ${report.wiki.documents} documents, ${report.wiki.revisions} revisions`
32636
+ );
32637
+ console.log(
32638
+ `${sym.info} Wiki cleanup candidates: ${report.wiki.unreachableSnapshots} unreachable snapshots, ${report.wiki.unreachableTransactions} unreachable transactions, ${report.wiki.unreachableDocuments} unreachable documents, ${report.wiki.unreachableRevisions} unreachable revisions`
32639
+ );
32640
+ console.log(
32641
+ `${sym.info} Wiki duplicates: ${report.wiki.duplicateSemanticSnapshots} semantic snapshots, ${report.wiki.duplicateSemanticDocuments} semantic documents`
32642
+ );
32643
+ console.log(
32644
+ `${sym.info} Broken references: ${report.brokenReferences.length}`
32645
+ );
32646
+ for (const broken of report.brokenReferences.slice(0, 20)) {
32647
+ console.log(` ${broken}`);
32648
+ }
32649
+ if (report.brokenReferences.length > 20) {
32650
+ console.log(
32651
+ ` ... ${report.brokenReferences.length - 20} additional broken references`
32652
+ );
32653
+ }
32654
+ }
32655
+ function printPlan(plan) {
32656
+ const scope = plan.versionId === null ? plan.projectId : `${plan.projectId}/${plan.versionId}`;
32657
+ console.log(`${sym.info} Scope: ${scope}`);
32658
+ console.log(`${sym.info} Mode: ${plan.mode}`);
32659
+ console.log(
32660
+ `${sym.info} Project records: ${plan.counts.removableSnapshots} snapshots and ${plan.counts.removableTransactions} transactions removable`
32661
+ );
32662
+ if (plan.wikiCounts !== null) {
32663
+ console.log(
32664
+ `${sym.info} Wiki: ${plan.wikiCounts.removableSnapshots} snapshots, ${plan.wikiCounts.removableTransactions} transactions, and ${plan.wikiCounts.removableDocuments} documents removable`
32665
+ );
32666
+ }
32667
+ console.log(
32668
+ `${sym.info} Current baseline: project=${String(plan.compactionCurrent)} wiki=${String(plan.wikiCompactionCurrent)}`
32669
+ );
32670
+ console.log(`Apply with: neo history apply --plan ${plan.id}`);
32671
+ }
32672
+ function printRunStatus(status) {
32673
+ const scope = status.versionId === null ? status.projectId : `${status.projectId}/${status.versionId}`;
32674
+ console.log(`${sym.info} Run: ${status.runId}`);
32675
+ console.log(`${sym.info} Plan: ${status.planId}`);
32676
+ console.log(`${sym.info} Scope: ${scope}`);
32677
+ console.log(`${sym.info} State: ${status.state} (${status.phase})`);
32678
+ console.log(
32679
+ `${sym.info} Correctness verification: ${status.verified ? "verified" : "pending"}`
32680
+ );
32681
+ if (status.lastError !== null) {
32682
+ console.log(`${sym.info} Error: ${status.lastError}`);
32683
+ }
32684
+ }
32685
+ function assertHistoryOptions(options) {
32686
+ const planningOrRead = options.command === "inspect" || options.command === "log" || options.command === "prune" || options.command === "flatten";
32687
+ if (planningOrRead) {
32688
+ if (options.planId !== null || options.runId !== null) {
32689
+ throw new Error(
32690
+ `neo history ${options.command} does not accept --plan or --run.`
32691
+ );
32692
+ }
32693
+ if (options.confirmScope !== null) {
32694
+ throw new Error(
32695
+ `neo history ${options.command} does not accept --confirm-scope.`
32696
+ );
32697
+ }
32698
+ if ((options.command === "inspect" || options.command === "prune") && options.versionSelector !== null) {
32699
+ throw new Error(
32700
+ `neo history ${options.command} is project-scoped and does not accept --version.`
32701
+ );
32702
+ }
32703
+ return;
32704
+ }
32705
+ if (options.command === "apply") {
32706
+ if (options.planId === null) {
32707
+ throw new Error("neo history apply requires --plan <plan-id>.");
32708
+ }
32709
+ if (options.runId !== null) {
32710
+ throw new Error("neo history apply does not accept --run.");
32711
+ }
32712
+ return;
32713
+ }
32714
+ if (options.runId === null) {
32715
+ throw new Error(`neo history ${options.command} requires --run <run-id>.`);
32716
+ }
32717
+ if (options.planId !== null || options.confirmScope !== null) {
32718
+ throw new Error(
32719
+ `neo history ${options.command} does not accept --plan or --confirm-scope.`
32720
+ );
32721
+ }
32722
+ }
32723
+ var init_history = __esm({
32724
+ "src/commands/history.ts"() {
32725
+ "use strict";
32726
+ init_convex();
32727
+ init_ui();
32728
+ init_history_selection();
32729
+ init_history_inspect();
32730
+ }
32731
+ });
32732
+
32165
32733
  // src/commands/doctor.ts
32166
32734
  var doctor_exports = {};
32167
32735
  __export(doctor_exports, {
@@ -44574,16 +45142,19 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
44574
45142
  "out",
44575
45143
  "profile",
44576
45144
  "project",
45145
+ "plan",
44577
45146
  "push",
44578
45147
  "replace",
44579
45148
  "reset",
44580
45149
  "returns",
45150
+ "run",
44581
45151
  "save",
44582
45152
  "save-project",
44583
45153
  "server",
44584
45154
  "skip-invalid",
44585
45155
  "status",
44586
45156
  "summary",
45157
+ "confirm-scope",
44587
45158
  "target",
44588
45159
  "template",
44589
45160
  "theirs",
@@ -44681,6 +45252,7 @@ ${h("Branches & releases")}
44681
45252
  merge ${d("[source] [--dry-run] [--migrate] [--mine|--theirs <kind>:<id>,...]")}
44682
45253
  release ${d("cut [--bump ...] | publish <ref> | archive <ref> | restore <ref>")}
44683
45254
  channel ${d("list | create | edit | delete")}
45255
+ history ${d("inspect | log | prune | flatten | apply | status | resume")}
44684
45256
 
44685
45257
  ${h("Content & scripts")}
44686
45258
  values ${d("list | get | set | create | delete | ...")} loc ${d("locales | list | set | archive | restore | ...")}
@@ -44748,6 +45320,46 @@ ${h("Usage")}
44748
45320
  Neo checks Unity 6000's existing runtime first, then a compatible system
44749
45321
  dotnet. It never downloads or installs a runtime. Set NEO_DOTNET_HOST to use
44750
45322
  an explicit host.
45323
+ `;
45324
+ }
45325
+ if (command === "history") {
45326
+ return `${h("neo history")} \u2014 inspect and deliberately rewrite project history
45327
+
45328
+ ${h("Usage")}
45329
+ neo history inspect ${d("[--project <id|@latest|@current>]")}
45330
+ neo history log ${d("[--project <id|@latest|@current>] [--version <id|@latest|@current>]")}
45331
+ neo history prune ${d("[--project <id|@latest|@current>]")}
45332
+ neo history flatten ${d("[--project <id|@latest|@current>] [--version <id|@latest|@current>]")}
45333
+ neo history apply --plan <plan-id> ${d("[--confirm-scope <project-id>[/<version-id>]]")}
45334
+ neo history status --run <run-id>
45335
+ neo history resume --run <run-id>
45336
+
45337
+ ${h("Commands")}
45338
+ inspect ${d("Read-only project history audit; creates no plan.")}
45339
+ log ${d("Show retained history for exactly one version.")}
45340
+ prune ${d("Create an immutable plan for rows proven unreachable; writes nothing.")}
45341
+ flatten ${d("Create an immutable one-version baseline plan; writes nothing.")}
45342
+ apply ${d("Begin the destructive execution of an immutable plan.")}
45343
+ status ${d("Report execution phase, result, and correctness verification.")}
45344
+ resume ${d("Restart a stopped retryable execution.")}
45345
+
45346
+ ${h("Arguments")}
45347
+ --project <id|@latest|@current>
45348
+ ${d("Stable project ID, or latest authorized project. Blank interactive input uses latest.")}
45349
+ --version <id|@latest|@current>
45350
+ ${d("Stable version ID in the resolved project, or latest unarchived version. Blank interactive input uses latest.")}
45351
+ --plan <id>
45352
+ ${d("Immutable plan returned by prune or flatten.")}
45353
+ --run <id>
45354
+ ${d("Execution returned by apply.")}
45355
+ --confirm-scope <project>[/<version>]
45356
+ ${d("Exact resolved production scope acknowledgement.")}
45357
+
45358
+ ${h("Selection")}
45359
+ ${d("Project resolution always happens before version resolution. @latest and @current are aliases. apply, status, and resume use immutable plan/run scope and do not accept project or version selectors.")}
45360
+
45361
+ There is no --dry-run: prune and flatten are always non-mutating until apply.
45362
+ There is no --keep-current: preserving current semantic state defines flatten.
44751
45363
  `;
44752
45364
  }
44753
45365
  return usage();
@@ -44788,6 +45400,30 @@ async function main() {
44788
45400
  });
44789
45401
  }
44790
45402
  return;
45403
+ case "history": {
45404
+ const sub = args.positional[0];
45405
+ if (sub !== "inspect" && sub !== "log" && sub !== "prune" && sub !== "flatten" && sub !== "apply" && sub !== "status" && sub !== "resume") {
45406
+ throw new Error(
45407
+ "neo history requires a subcommand: inspect | log | prune | flatten | apply | status | resume."
45408
+ );
45409
+ }
45410
+ if ((sub === "apply" || sub === "status" || sub === "resume") && (stringFlag(args, "project") !== null || stringFlag(args, "version") !== null)) {
45411
+ throw new Error(
45412
+ `neo history ${sub} derives its immutable scope from the plan or run and does not accept --project or --version.`
45413
+ );
45414
+ }
45415
+ const { runHistory: runHistory2 } = await Promise.resolve().then(() => (init_history(), history_exports));
45416
+ await runHistory2({
45417
+ apiBaseUrl,
45418
+ command: sub,
45419
+ projectSelector: stringFlag(args, "project"),
45420
+ versionSelector: stringFlag(args, "version"),
45421
+ planId: stringFlag(args, "plan"),
45422
+ runId: stringFlag(args, "run"),
45423
+ confirmScope: stringFlag(args, "confirm-scope")
45424
+ });
45425
+ return;
45426
+ }
44791
45427
  case "pull": {
44792
45428
  const workspace = loadWorkspaceForCommand(args);
44793
45429
  const { runPull: runPull2 } = await Promise.resolve().then(() => (init_pull(), pull_exports));
Binary file
Binary file
@@ -4,7 +4,7 @@
4
4
  "manifestName": "neo-schema-manifest-v3",
5
5
  "protocolVersion": 4,
6
6
  "sdkVersion": "3.0.0",
7
- "cliVersion": "0.3.0",
7
+ "cliVersion": "0.3.1",
8
8
  "apiTarget": "netstandard2.1",
9
9
  "referenceAssemblySet": "NETStandard.Library.Ref/2.1.0",
10
10
  "requiredToolingFiles": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Neo Compose schema-as-code CLI with compiler-backed C# authoring and bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",