@stage5/lumine 0.2.13 → 0.2.14

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/README.md CHANGED
@@ -40,7 +40,14 @@ to pull source files into a read-only reference folder, or
40
40
  `lumine fork <build-url-or-id>` to create your own editable fork and pull it
41
41
  locally.
42
42
 
43
- After editing pulled files, run `lumine save` from that folder. The CLI saves
43
+ After editing pulled files, run `lumine save` from that folder. Workspaces record
44
+ a `filesHash` in `.twinkle/lumine-project.json` when you pull or save; the next
45
+ save sends that hash so the server can reject a stale checkout instead of
46
+ silently overwriting newer files. Saves from a folder with no `filesHash` are
47
+ refused unless you pass `--force` (intentional overwrite). Prefer
48
+ `lumine pull` to resync before saving if the folder might be out of date.
49
+
50
+ The CLI saves
44
51
  through Twinkle's normal workspace project-file route, creates a project artifact
45
52
  version, records the same save metadata, and marks public builds as having
46
53
  unpublished changes. For projects you own, run `lumine launch` to publish the
package/lib/api.js CHANGED
@@ -139,6 +139,7 @@ export async function saveProjectFiles({
139
139
  files,
140
140
  summary,
141
141
  baseFilesHash,
142
+ force = false,
142
143
  }) {
143
144
  return await requestJson({
144
145
  method: "PUT",
@@ -152,6 +153,9 @@ export async function saveProjectFiles({
152
153
  // reject it instead of silently rewinding newer state (e.g. a branch
153
154
  // merged into main after our pull).
154
155
  ...(baseFilesHash ? { baseFilesHash } : {}),
156
+ // Intentional overwrite of a non-empty project without a base claim.
157
+ // Required when the workspace has no filesHash (old/stale checkout).
158
+ ...(force ? { force: true } : {}),
155
159
  },
156
160
  timeoutMs: options.timeoutMs,
157
161
  });
package/lib/commands.js CHANGED
@@ -509,13 +509,36 @@ export async function save(options) {
509
509
  const files = await collectProjectFiles(dir);
510
510
  assertProjectFilesWithinLimits(files);
511
511
  // Only claim a base snapshot when this workspace was pulled from the same
512
- // build we are saving to; otherwise the save stays unguarded (old behavior).
512
+ // build we are saving to. Missing filesHash used to mean "unguarded save",
513
+ // which let stale checkouts silently rewind newer server versions — refuse
514
+ // that path unless --force is explicit.
515
+ const metadataBuildId = Number(localProject?.metadata?.buildId || 0);
516
+ const metadataFilesHash =
517
+ typeof localProject?.metadata?.filesHash === "string" &&
518
+ localProject.metadata.filesHash.trim()
519
+ ? localProject.metadata.filesHash.trim()
520
+ : null;
513
521
  const baseFilesHash =
514
- !options.force &&
515
- Number(localProject?.metadata?.buildId || 0) === Number(buildId) &&
516
- typeof localProject?.metadata?.filesHash === "string"
517
- ? localProject.metadata.filesHash
522
+ !options.force && metadataBuildId === Number(buildId) && metadataFilesHash
523
+ ? metadataFilesHash
518
524
  : null;
525
+ if (!options.force && !baseFilesHash) {
526
+ throw new Error(
527
+ [
528
+ "Save refused: this workspace has no filesHash (server snapshot token).",
529
+ "That usually means an old or stale checkout that never recorded what",
530
+ "it was based on — saving it would overwrite the server without a guard.",
531
+ "Run `lumine pull` to sync with the server (local edits are preserved),",
532
+ "then save again. To intentionally overwrite the server with these",
533
+ "local files, re-run with --force.",
534
+ ].join("\n"),
535
+ );
536
+ }
537
+ if (options.force && !baseFilesHash) {
538
+ console.error(
539
+ "lumine: warning — saving with --force and no filesHash; this overwrites the server project with local files and skips the stale-workspace guard.",
540
+ );
541
+ }
519
542
  let result;
520
543
  try {
521
544
  result = await saveProjectFiles({
@@ -525,6 +548,7 @@ export async function save(options) {
525
548
  files,
526
549
  summary: options.summary || DEFAULT_SAVE_SUMMARY,
527
550
  baseFilesHash,
551
+ force: Boolean(options.force),
528
552
  });
529
553
  } catch (error) {
530
554
  if (error?.data?.code === "build_project_files_stale") {
@@ -537,6 +561,17 @@ export async function save(options) {
537
561
  ].join("\n"),
538
562
  );
539
563
  }
564
+ if (error?.data?.code === "build_project_files_base_required") {
565
+ throw new Error(
566
+ [
567
+ "Save rejected: the server requires a filesHash base for this project",
568
+ "(non-empty projects cannot be saved from a workspace that never",
569
+ "recorded its server snapshot).",
570
+ "Run `lumine pull` to establish a base, then save again,",
571
+ "or re-run with --force to overwrite deliberately.",
572
+ ].join("\n"),
573
+ );
574
+ }
540
575
  throw error;
541
576
  }
542
577
  build = result.build ||
@@ -1954,7 +1989,7 @@ Options:
1954
1989
  --description <text> Optional New Build description
1955
1990
  --no-description Skip the New Build description prompt
1956
1991
  --summary <text> Save summary
1957
- --force Save even if the server files changed since this workspace was pulled
1992
+ --force Overwrite server files even if this workspace is stale or missing filesHash
1958
1993
  --search <text> Search public open-source Builds
1959
1994
  --sort <sort> Sort open-source Builds: forks, popular, recent
1960
1995
  --publish Publish after saving
package/lib/constants.js CHANGED
@@ -143,6 +143,12 @@ Lumine CLI as the source of truth for saving this workspace back to Twinkle.
143
143
 
144
144
  - Edit only project files in this workspace.
145
145
  - Keep /index.html or /index.htm as the entry file.
146
+ - Before editing an existing project, confirm \`.twinkle/lumine-project.json\`
147
+ has a \`filesHash\` and matches the build you intend. If the folder may be
148
+ stale (old pull, missing files, fewer files than last known save), run
149
+ \`lumine pull\` first — never \`lumine save\` from a guessed/old checkout.
150
+ Saves without \`filesHash\` are refused (use \`--force\` only for deliberate
151
+ overwrites). A stale save can delete newer server files.
146
152
  - Run lumine save from this folder after edits, with a short summary:
147
153
 
148
154
  \`\`\`bash
package/lib/workspace.js CHANGED
@@ -597,7 +597,8 @@ export async function writeProjectMetadata({
597
597
  lastSavedAt,
598
598
  // Server-issued hash of the project files this workspace is based on.
599
599
  // `lumine save` sends it back so a stale workspace cannot silently
600
- // rewind newer server state; null means "unknown" (unguarded save).
600
+ // rewind newer server state. Missing/null refuses save unless --force
601
+ // (server also rejects unguarded saves onto non-empty projects).
601
602
  filesHash: typeof filesHash === "string" ? filesHash : null,
602
603
  },
603
604
  null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {