@neocompose/cli 0.21.2 → 0.21.3

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.21.3] - 2026-08-03
4
+
5
+ ### Fixed
6
+
7
+ - `neo push` paints its preparation spinner before workspace discovery,
8
+ lowering, and NeoScript compilation begin, so large projects no longer look
9
+ stalled before the push plan appears.
10
+ - Animation segment frame values are emitted through the required `value`
11
+ constructor argument. Required sprites use the canonical `SpriteInfo.Empty`
12
+ value; `null` is reserved for nullable `SpriteInfo?` values.
13
+
3
14
  ## [0.21.2] - 2026-08-03
4
15
 
5
16
  ### Fixed
package/dist/neo.mjs CHANGED
@@ -842,7 +842,7 @@ function vectorTypes() {
842
842
  members: structuredLeafFieldProperties(name)
843
843
  }));
844
844
  }
845
- var NEOSCRIPT_KEYWORDS, NEOSCRIPT_INFERRED_LOCAL_KEYWORD, NEOSCRIPT_CONSTRUCTOR_KEYWORD, NEOSCRIPT_PRIMITIVE_TYPES, NEOSCRIPT_BUILTIN_TYPES, NEOSCRIPT_OPERATORS, NEOSCRIPT_STATEMENT_SNIPPETS, STRING_TYPE, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, IMAGE_REF_TYPE, FLOAT_OPTIONAL_TYPE, NEOSCRIPT_STRUCTURED_LEAF_FIELDS, NEOSCRIPT_IMAGE_REGISTRY_TYPE, NEOSCRIPT_AUDIO_CLIP_REGISTRY_TYPE, NEOSCRIPT_PENDING_SYMBOL_ID_PREFIX, REGISTRY_ENTRY_TYPES, NEOSCRIPT_GLOBALS, NEOSCRIPT_BUILTIN_TYPE_SYMBOLS;
845
+ var NEOSCRIPT_KEYWORDS, NEOSCRIPT_INFERRED_LOCAL_KEYWORD, NEOSCRIPT_CONSTRUCTOR_KEYWORD, NEOSCRIPT_PRIMITIVE_TYPES, NEOSCRIPT_BUILTIN_TYPES, NEOSCRIPT_OPERATORS, NEOSCRIPT_STATEMENT_SNIPPETS, STRING_TYPE, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, IMAGE_REF_TYPE, SPRITE_INFO_TYPE, FLOAT_OPTIONAL_TYPE, NEOSCRIPT_STRUCTURED_LEAF_FIELDS, NEOSCRIPT_IMAGE_REGISTRY_TYPE, NEOSCRIPT_AUDIO_CLIP_REGISTRY_TYPE, NEOSCRIPT_PENDING_SYMBOL_ID_PREFIX, REGISTRY_ENTRY_TYPES, NEOSCRIPT_GLOBALS, NEOSCRIPT_SPRITE_INFO_EMPTY_MEMBER_ID, NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE, NEOSCRIPT_BUILTIN_TYPE_SYMBOLS;
846
846
  var init_language_spec = __esm({
847
847
  "../packages/neoscript-language/src/language-spec.ts"() {
848
848
  "use strict";
@@ -1006,6 +1006,10 @@ var init_language_spec = __esm({
1006
1006
  kind: "primitive",
1007
1007
  name: "ImageRef"
1008
1008
  };
1009
+ SPRITE_INFO_TYPE = {
1010
+ kind: "primitive",
1011
+ name: "SpriteInfo"
1012
+ };
1009
1013
  FLOAT_OPTIONAL_TYPE = {
1010
1014
  kind: "primitive",
1011
1015
  name: "float",
@@ -1148,12 +1152,26 @@ var init_language_spec = __esm({
1148
1152
  "Returns whether a dialogue pointer has been visited."
1149
1153
  )
1150
1154
  ];
1155
+ NEOSCRIPT_SPRITE_INFO_EMPTY_MEMBER_ID = "builtin:SpriteInfo:property:Empty";
1156
+ NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE = Object.freeze({
1157
+ fileId: "",
1158
+ sliceIndex: 0
1159
+ });
1151
1160
  NEOSCRIPT_BUILTIN_TYPE_SYMBOLS = [
1152
1161
  {
1153
1162
  id: "builtin:SpriteInfo",
1154
1163
  name: "SpriteInfo",
1155
1164
  kind: "builtin",
1156
1165
  members: [
1166
+ {
1167
+ ...property(
1168
+ "SpriteInfo",
1169
+ "Empty",
1170
+ SPRITE_INFO_TYPE,
1171
+ "An authored empty sprite at slice zero."
1172
+ ),
1173
+ static: true
1174
+ },
1157
1175
  // `FileId` and `SliceIndex` come from the field table above — they are
1158
1176
  // the writable, addressable fields, not derived properties (D11).
1159
1177
  ...structuredLeafFieldProperties("SpriteInfo"),
@@ -9739,6 +9757,12 @@ var init_strict_resolver = __esm({
9739
9757
  }
9740
9758
  resolveMemberNatural(receiverAst, name, scope, pos, optional) {
9741
9759
  if (receiverAst.kind === "ident" && !scope.lookup(receiverAst.name)) {
9760
+ if (receiverAst.name === "SpriteInfo" && name === "Empty") {
9761
+ return literal(
9762
+ { kind: "primitive", name: "SpriteInfo" },
9763
+ { ...NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE }
9764
+ );
9765
+ }
9742
9766
  const staticType = this.project.typeByName.get(receiverAst.name);
9743
9767
  if (staticType && staticType.kind !== "enum") {
9744
9768
  if (optional) {
@@ -9769,6 +9793,11 @@ var init_strict_resolver = __esm({
9769
9793
  pos
9770
9794
  );
9771
9795
  }
9796
+ if (member.id === NEOSCRIPT_SPRITE_INFO_EMPTY_MEMBER_ID) {
9797
+ return literal(member.type, {
9798
+ ...NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE
9799
+ });
9800
+ }
9772
9801
  if (member.computed === true) {
9773
9802
  return {
9774
9803
  pointer: {
@@ -38004,10 +38033,10 @@ function isSpriteValue(value) {
38004
38033
  if (!Number.isInteger(sliceIndex)) return false;
38005
38034
  return sliceIndex >= 0;
38006
38035
  }
38007
- function pendingSpriteValue() {
38036
+ function emptySpriteValue() {
38008
38037
  return { fileId: "", sliceIndex: 0 };
38009
38038
  }
38010
- function isPendingSpriteValue(value) {
38039
+ function isEmptySpriteValue(value) {
38011
38040
  if (!value || typeof value !== "object") return false;
38012
38041
  return getField(value, "fileId") === "" && getField(value, "sliceIndex") === 0;
38013
38042
  }
@@ -38910,6 +38939,7 @@ var init_world_system_classes_generated = __esm({
38910
38939
  {
38911
38940
  memberId: "system_cbd6db9a-f473-44b5-b913-7cdc06452f35",
38912
38941
  memberKind: "sprite",
38942
+ defaultValue: { fileId: "", sliceIndex: 0 },
38913
38943
  required: true,
38914
38944
  schemaKey: "Sprite"
38915
38945
  },
@@ -39032,6 +39062,7 @@ var init_world_system_classes_generated = __esm({
39032
39062
  {
39033
39063
  memberId: "system_e9288ba9-f5a2-4485-8443-6afb155b31e0",
39034
39064
  memberKind: "sprite",
39065
+ defaultValue: { fileId: "", sliceIndex: 0 },
39035
39066
  required: true,
39036
39067
  schemaKey: "Sprite"
39037
39068
  },
@@ -39516,6 +39547,12 @@ var init_world_system_classes_generated = __esm({
39516
39547
  docsText: "One value at a frame index inside a segment. It holds until the next row\nyou author, or until the segment's Duration runs out.",
39517
39548
  extendsWorldKind: "animationFrameBase",
39518
39549
  isAbstract: false,
39550
+ constructorProjections: [
39551
+ {
39552
+ memberId: "system_b71d1f73-da72-49de-ab9a-911cc9bffa43",
39553
+ parameterName: "value"
39554
+ }
39555
+ ],
39519
39556
  genericParams: [
39520
39557
  { id: "system_887b11d3-e56a-47ff-8d01-e2fcefb846a1", name: "T" }
39521
39558
  ],
@@ -42586,7 +42623,7 @@ function stampGenericBindings(value, member, args) {
42586
42623
  }
42587
42624
  function shouldSkipRequiredChildDefault(member) {
42588
42625
  if (member.defaultValue !== void 0) return false;
42589
- return isMemberSpriteBase(member) || isMemberAudioBase(member);
42626
+ return isMemberAudioBase(member);
42590
42627
  }
42591
42628
  function shouldMaterializeChildDefault(member) {
42592
42629
  if (isMemberClassBase(member) && member.partial === true) return false;
@@ -42658,7 +42695,7 @@ function primitiveFallbackValue(document, member) {
42658
42695
  if (isMemberLookupBase(member)) return { value: [] };
42659
42696
  if (isMemberDialogueLookupBase(member)) return { value: [] };
42660
42697
  if (isMemberSpriteBase(member)) {
42661
- return { value: pendingSpriteValue() };
42698
+ return { value: emptySpriteValue() };
42662
42699
  }
42663
42700
  if (isMemberAudioBase(member)) {
42664
42701
  return { value: pendingAudioValue() };
@@ -42682,7 +42719,7 @@ function primitiveFallbackValue(document, member) {
42682
42719
  function isPendingFileMemberValue(member, body) {
42683
42720
  const value = body.value;
42684
42721
  if (isMemberSpriteBase(member)) {
42685
- return isPendingSpriteValue(value);
42722
+ return isEmptySpriteValue(value);
42686
42723
  }
42687
42724
  if (isMemberAudioBase(member)) {
42688
42725
  return isPendingAudioValue(value);
@@ -42768,11 +42805,18 @@ function validateDecimalMemberValue(member, body) {
42768
42805
  }
42769
42806
  function validateSpriteMemberValue(document, member, body) {
42770
42807
  const spriteValue = body.value;
42808
+ if (spriteValue === null) {
42809
+ if (!member.required) return;
42810
+ throw new Error(
42811
+ `Sprite member "${member.name}" is required and cannot be null.`
42812
+ );
42813
+ }
42771
42814
  if (!isSpriteValue(spriteValue)) {
42772
42815
  throw new Error(
42773
42816
  "Sprite value must be { fileId: string, sliceIndex: integer >= 0 }."
42774
42817
  );
42775
42818
  }
42819
+ if (isEmptySpriteValue(spriteValue)) return;
42776
42820
  validateSpriteFileRegistration(document, member, spriteValue.fileId);
42777
42821
  }
42778
42822
  function validateSpriteFileRegistration(document, member, fileId) {
@@ -44993,6 +45037,9 @@ function lowerExpressionValue(context, expression, declaredExpected, ownerClass,
44993
45037
  }
44994
45038
  switch (expression.kind) {
44995
45039
  case "litNull":
45040
+ if (!expected.nullable) {
45041
+ throw new Error(`Required value ${path} cannot be null.`);
45042
+ }
44996
45043
  return null;
44997
45044
  case "litBool":
44998
45045
  case "litString":
@@ -45212,6 +45259,9 @@ function lowerExpressionValue(context, expression, declaredExpected, ownerClass,
45212
45259
  );
45213
45260
  }
45214
45261
  case "member": {
45262
+ if (expected.name === "SpriteInfo" && expression.name === "Empty" && expression.receiver.kind === "ident" && expression.receiver.name === "SpriteInfo") {
45263
+ return { fileId: "", sliceIndex: 0 };
45264
+ }
45215
45265
  const fileId = projectFileIdForPath(context, expression);
45216
45266
  if (fileId !== null) return { fileId };
45217
45267
  if (expression.name === "Slice" || expression.receiver.kind === "call") {
@@ -77143,7 +77193,12 @@ function lowerValueRow(context, member, sourceExpression, expectedValueId, sourc
77143
77193
  }
77144
77194
  function lowerValueBody(context, member, expression, base, source, environment, authoredSlice) {
77145
77195
  const next = valueFileFields(base);
77146
- if (expression.kind === "litNull") return { ...next, value: null };
77196
+ if (expression.kind === "litNull") {
77197
+ if (member.required) {
77198
+ throw new Error(`Required member ${member.name} cannot store null.`);
77199
+ }
77200
+ return { ...next, value: null };
77201
+ }
77147
77202
  switch (member.kind) {
77148
77203
  case "null":
77149
77204
  return { ...next, value: null };
@@ -78127,6 +78182,9 @@ function expressionAnchorPos(expression) {
78127
78182
  }
78128
78183
  function lowerFileValue(context, member, expression) {
78129
78184
  if (member.kind === "sprite") {
78185
+ if (isSpriteInfoEmptyExpression(expression)) {
78186
+ return { fileId: "", sliceIndex: 0 };
78187
+ }
78130
78188
  if (expression.kind !== "call" || expression.callee.kind !== "member" || expression.callee.name !== "Slice") {
78131
78189
  throw new Error(
78132
78190
  `Sprite member ${member.name} requires Image.Slice(index).`
@@ -79114,7 +79172,8 @@ function emitValueBody(context, member, value, visited, targetTyped, environment
79114
79172
  context,
79115
79173
  kind,
79116
79174
  `Audio member ${JSON.stringify(String(member.name ?? member.id))}`,
79117
- body
79175
+ body,
79176
+ member.required === true
79118
79177
  );
79119
79178
  }
79120
79179
  if (kind === 5)
@@ -79364,7 +79423,12 @@ function storedValueConstructor(context, schemaClass2, constructorArgs) {
79364
79423
  return selected2;
79365
79424
  }
79366
79425
  function storedConstructorArgumentSource(context, type, value, environment, visited, cloneAggregateArguments = false) {
79367
- if (value === null) return "null";
79426
+ if (value === null) {
79427
+ if (!type.nullable) {
79428
+ throw new Error("Required constructor argument stores null.");
79429
+ }
79430
+ return "null";
79431
+ }
79368
79432
  switch (type.kind) {
79369
79433
  case "null":
79370
79434
  return "null";
@@ -79396,7 +79460,8 @@ function storedConstructorArgumentSource(context, type, value, environment, visi
79396
79460
  {
79397
79461
  id: "__constructor_arg__",
79398
79462
  name: "argument",
79399
- kind: 11 /* Sprite */
79463
+ kind: 11 /* Sprite */,
79464
+ required: !type.nullable
79400
79465
  },
79401
79466
  value
79402
79467
  );
@@ -79405,7 +79470,8 @@ function storedConstructorArgumentSource(context, type, value, environment, visi
79405
79470
  context,
79406
79471
  12 /* Audio */,
79407
79472
  "constructor argument",
79408
- value
79473
+ value,
79474
+ !type.nullable
79409
79475
  );
79410
79476
  case "enum":
79411
79477
  return enumValue(
@@ -80180,8 +80246,13 @@ function delegateValue(context, body) {
80180
80246
  }
80181
80247
  return target.name;
80182
80248
  }
80183
- function fileValue(context, kind, label, body) {
80184
- if (body === null || body === void 0) return "null";
80249
+ function fileValue(context, kind, label, body, required2) {
80250
+ if (body === null || body === void 0) {
80251
+ if (required2) {
80252
+ throw new Error(`${label} is required and cannot store null.`);
80253
+ }
80254
+ return "null";
80255
+ }
80185
80256
  if (!isObjectRecord2(body)) {
80186
80257
  throw new Error(
80187
80258
  `${label} stores ${JSON.stringify(body)}, which is not a file value record.`
@@ -80193,12 +80264,18 @@ function fileValue(context, kind, label, body) {
80193
80264
  );
80194
80265
  }
80195
80266
  if (body.fileId.length === 0) {
80196
- throw new Error(`${label} stores an empty fileId.`);
80267
+ if (kind === 11 && numberOr2(body.sliceIndex, 0) === 0) {
80268
+ return "SpriteInfo.Empty";
80269
+ }
80270
+ throw new Error(`${label} stores an invalid empty file value.`);
80197
80271
  }
80198
80272
  const base = fileRegistrySource(context, kind, body.fileId);
80199
80273
  if (kind !== 11) return base;
80200
80274
  return `${base}.Slice(${numberOr2(body.sliceIndex, 0)})`;
80201
80275
  }
80276
+ function isSpriteInfoEmptyExpression(expression) {
80277
+ return expression.kind === "member" && expression.name === "Empty" && expression.receiver.kind === "ident" && expression.receiver.name === "SpriteInfo";
80278
+ }
80202
80279
  function fileRegistrySource(context, kind, fileId) {
80203
80280
  const symbol = context.fileSymbols.get(fileId);
80204
80281
  if (symbol !== void 0) {
@@ -80223,7 +80300,9 @@ function structuredLeafSource(context, kind, member, body) {
80223
80300
  ${assignments.map((entry) => indentNeoSourceNonEmptyLines(entry, 2)).join(",\n")}
80224
80301
  }`;
80225
80302
  }
80226
- if (kind === 11 /* Sprite */) return fileValue(context, 11, label, body);
80303
+ if (kind === 11 /* Sprite */) {
80304
+ return fileValue(context, 11, label, body, member.required === true);
80305
+ }
80227
80306
  if (body === null) return "null";
80228
80307
  if (!isObjectRecord2(body)) return "new()";
80229
80308
  return `new(${structuredLeafFieldKeys(kind).map((key) => scalar(body[key])).join(", ")})`;
@@ -94858,7 +94937,7 @@ async function downloadProjectVersionTransactionResult(args) {
94858
94937
  cursor = page.continueCursor;
94859
94938
  }
94860
94939
  }
94861
- async function runPush(workspace, options) {
94940
+ async function preparePushStatus(workspace, options) {
94862
94941
  const status = computeWorkspaceStatus(workspace);
94863
94942
  if (status.conflictedFiles.length > 0) {
94864
94943
  throw new Error(
@@ -94887,22 +94966,7 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
94887
94966
  changes: status.changes,
94888
94967
  binaryChanges: status.binaryChanges ?? []
94889
94968
  });
94890
- if (status.changes.length === 0) {
94891
- if (options.json === true) {
94892
- console.log(
94893
- JSON.stringify({
94894
- type: "push-plan",
94895
- dryRun: options.dryRun,
94896
- changeCount: 0,
94897
- staticValueSeedCount: 0,
94898
- changes: []
94899
- })
94900
- );
94901
- } else {
94902
- console.log("Nothing to push \u2014 working copy is clean.");
94903
- }
94904
- return;
94905
- }
94969
+ if (status.changes.length === 0) return status;
94906
94970
  const initConversions = status.initConversions ?? [];
94907
94971
  if (initConversions.length > 0 && options.json !== true) {
94908
94972
  for (const line of describeInitConversions(initConversions)) warn(line);
@@ -94974,9 +95038,41 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
94974
95038
  } catch (error) {
94975
95039
  reportDryRunFailure(error, options.json === true);
94976
95040
  process.exitCode = 1;
94977
- return;
95041
+ return null;
94978
95042
  }
94979
95043
  }
95044
+ return status;
95045
+ }
95046
+ async function runPush(workspace, options, preparationOverride) {
95047
+ const preparation = preparationOverride === void 0 ? process.stdout.isTTY === true && options.json !== true ? spinner("Preparing push\u2026") : null : preparationOverride;
95048
+ let status;
95049
+ try {
95050
+ status = await preparePushStatus(workspace, options);
95051
+ } catch (error) {
95052
+ preparation?.fail("Push preparation failed.");
95053
+ throw error;
95054
+ }
95055
+ if (status === null) {
95056
+ preparation?.fail("Push preparation failed.");
95057
+ return;
95058
+ }
95059
+ preparation?.stop();
95060
+ if (status.changes.length === 0) {
95061
+ if (options.json === true) {
95062
+ console.log(
95063
+ JSON.stringify({
95064
+ type: "push-plan",
95065
+ dryRun: options.dryRun,
95066
+ changeCount: 0,
95067
+ staticValueSeedCount: 0,
95068
+ changes: []
95069
+ })
95070
+ );
95071
+ } else {
95072
+ console.log("Nothing to push \u2014 working copy is clean.");
95073
+ }
95074
+ return;
95075
+ }
94980
95076
  if (options.json === true) {
94981
95077
  console.log(
94982
95078
  JSON.stringify({
@@ -98100,14 +98196,29 @@ async function main() {
98100
98196
  return;
98101
98197
  }
98102
98198
  case "push": {
98103
- const workspace = loadWorkspaceForCommand(args);
98104
- const { runPush: runPush2 } = await Promise.resolve().then(() => (init_push(), push_exports));
98105
- await runPush2(workspace, {
98106
- dryRun: boolFlag(args, "dry-run"),
98107
- summary: stringFlag(args, "summary"),
98108
- acceptBump: boolFlag(args, "accept-bump"),
98109
- json: boolFlag(args, "json")
98110
- });
98199
+ const json = boolFlag(args, "json");
98200
+ const preparation = process.stdout.isTTY === true && json !== true ? spinner("Preparing push\u2026") : null;
98201
+ let handedOffPreparation = false;
98202
+ try {
98203
+ const workspace = loadWorkspaceForCommand(args);
98204
+ const { runPush: runPush2 } = await Promise.resolve().then(() => (init_push(), push_exports));
98205
+ handedOffPreparation = true;
98206
+ await runPush2(
98207
+ workspace,
98208
+ {
98209
+ dryRun: boolFlag(args, "dry-run"),
98210
+ summary: stringFlag(args, "summary"),
98211
+ acceptBump: boolFlag(args, "accept-bump"),
98212
+ json
98213
+ },
98214
+ preparation
98215
+ );
98216
+ } catch (error) {
98217
+ if (!handedOffPreparation) {
98218
+ preparation?.fail("Push preparation failed.");
98219
+ }
98220
+ throw error;
98221
+ }
98111
98222
  return;
98112
98223
  }
98113
98224
  case "status": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.21.2",
3
+ "version": "0.21.3",
4
4
  "description": "Neo Compose native project-source CLI with bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ description: >-
9
9
  `@neocompose/cli` or `node cli/bin/neo.mjs` in the neo-compose repository.
10
10
  ---
11
11
 
12
- <!-- reviewed-through-cli: 0.21.2 -->
12
+ <!-- reviewed-through-cli: 0.21.3 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -66,12 +66,15 @@ Use complete values in normal member initialization:
66
66
 
67
67
  ```neo
68
68
  Sprite = Images.Hero.Slice(0);
69
+ Placeholder = SpriteInfo.Empty;
69
70
  Position = new(0, 0, 0);
70
71
  Tint = Color(1, 1, 1, 1);
71
72
  ```
72
73
 
73
74
  A bare `Images.Hero` yields a file ID only in a file-valued field context.
74
75
  Use `.Slice(index)` for a complete `SpriteInfo`.
76
+ Use `SpriteInfo.Empty` for an authored non-null empty sprite. Use `null` only
77
+ when the declared type is nullable `SpriteInfo?`.
75
78
 
76
79
  NeoScript may read or write structured fields when the receiver's effective
77
80
  storage permits it:
@@ -73,7 +73,7 @@ wrappers.
73
73
  The marker near the top of `SKILL.md` must exactly match the package version:
74
74
 
75
75
  ```html
76
- <!-- reviewed-through-cli: 0.21.2 -->
76
+ <!-- reviewed-through-cli: 0.21.3 -->
77
77
  ```
78
78
 
79
79
  The quoted version above is checked too, so this instruction cannot go stale
@@ -37,6 +37,10 @@ validation — so a rejection that used to cost a real push and return an opaque
37
37
  `preparation-failed` now reports before anything is sent. It does not prompt,
38
38
  upload, commit, or rewrite source.
39
39
 
40
+ Interactive pushes show `Preparing push…` before workspace discovery,
41
+ lowering, and compilation begin. JSON and non-TTY output remain structured and
42
+ do not emit that terminal-only spinner.
43
+
40
44
  A clean dry-run does not promise a clean push. The workspace holds its
41
45
  last-pulled state, so everything that grades the live head is still server-only:
42
46
  re-verification of the uploaded bundle bytes, project-file staging, the