@xano/cli 1.0.6-beta.17 → 1.0.6-beta.19

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
@@ -229,6 +229,21 @@ xano workspace git pull -r https://github.com/owner/private-repo -t ghp_xxx
229
229
  xano workspace git pull -r https://github.com/owner/repo --path subdir
230
230
  ```
231
231
 
232
+ **One workspace document per tree.** A push directory must contain at most one
233
+ `workspace/*.xs` document. The server applies the first workspace document it
234
+ receives to the workspace being pushed into and silently discards the rest,
235
+ without checking that it describes that workspace — so a second one renames the
236
+ destination to a foreign name while leaving its content untouched. `workspace push`
237
+ and `sandbox push` refuse a tree carrying more than one, naming the offending
238
+ files; there is no `--force` override, because only you know which workspace the
239
+ tree is meant to be.
240
+
241
+ Trees pick up a second one easily: `pull` names the file after the workspace
242
+ itself (`workspace/{name}.xs`), so pulling a different workspace into the same
243
+ directory *adds* a file rather than overwriting, and renaming a workspace leaves
244
+ the old-name file behind. Delete the stale ones, keeping the single document that
245
+ matches your target workspace.
246
+
232
247
  ### Knowledge
233
248
 
234
249
  Knowledge items are user-authored docs and skills (e.g. `CLAUDE.md`, `AGENTS.md`, runbooks)
@@ -702,7 +717,7 @@ xano tenant cluster license set <cluster_id> --file ./kubeconfig.yaml
702
717
 
703
718
  ### Ephemeral Tenants
704
719
 
705
- Manage ephemeral tenants — short-lived, auto-expiring tenants scoped to a workspace (default TTL 1h, max 72h). Unlike a sandbox, an ephemeral tenant requires a workspace.
720
+ Manage ephemeral tenants — short-lived, auto-expiring tenants scoped to a workspace (default TTL 1h, max 24h). Unlike a sandbox, an ephemeral tenant requires a workspace.
706
721
 
707
722
  ```bash
708
723
  # List ephemeral tenants in the current workspace
@@ -715,7 +730,7 @@ xano ephemeral list --global
715
730
  # Create an ephemeral tenant (workspace required)
716
731
  xano ephemeral create "PR preview"
717
732
  xano ephemeral create "Demo" --expires-hours 24 -w 5
718
- xano ephemeral create "Load test" -d "48h soak" --expires-hours 48
733
+ xano ephemeral create "Load test" -d "overnight soak" --expires-hours 24
719
734
 
720
735
  # Get / edit / delete
721
736
  xano ephemeral get <tenant_name>
@@ -14,7 +14,7 @@ Created ephemeral tenant: PR preview (e4f2-9ab1-...) - ID: 42
14
14
  Expires: in 1 hour
15
15
  `,
16
16
  `$ xano ephemeral create "Demo" --expires-hours 24 -w 114`,
17
- `$ xano ephemeral create "Load test" --description "48h soak" --expires-hours 48 -o json`,
17
+ `$ xano ephemeral create "Load test" --description "overnight soak" --expires-hours 24 -o json`,
18
18
  ];
19
19
  static flags = {
20
20
  ...BaseCommand.baseFlags,
@@ -25,8 +25,8 @@ Created ephemeral tenant: PR preview (e4f2-9ab1-...) - ID: 42
25
25
  }),
26
26
  'expires-hours': Flags.integer({
27
27
  default: 1,
28
- description: 'Hours until the tenant auto-expires (1-72)',
29
- max: 72,
28
+ description: 'Hours until the tenant auto-expires (1-24)',
29
+ max: 24,
30
30
  min: 1,
31
31
  required: false,
32
32
  }),
@@ -163,6 +163,28 @@ export declare function findMultiDocEntries(entries: Array<{
163
163
  count: number;
164
164
  filePath: string;
165
165
  }>;
166
+ /**
167
+ * Return every `workspace` document in the push set, with the name it declares.
168
+ *
169
+ * A tree must carry at most ONE. The server applies the first workspace
170
+ * document it encounters to the workspace being pushed into and silently
171
+ * discards the rest, with no check that the document actually describes that
172
+ * workspace — so a second one renames the destination to a foreign name while
173
+ * leaving its content untouched, and which name wins flips with the contents of
174
+ * the pushed set.
175
+ *
176
+ * Trees accumulate a second one easily, because `pull` names the file after the
177
+ * workspace's own name (`workspace/{name}.xs`): pulling a different workspace
178
+ * into the same directory ADDS a file rather than overwriting, and renaming a
179
+ * workspace leaves the old-name file behind.
180
+ */
181
+ export declare function findWorkspaceEntries(entries: Array<{
182
+ content: string;
183
+ filePath: string;
184
+ }>): Array<{
185
+ filePath: string;
186
+ name: string;
187
+ }>;
166
188
  export declare function renderBadReferences(badRefs: BadReference[], log: (msg: string) => void): void;
167
189
  export declare function renderBadIndexes(badIndexes: BadIndex[], log: (msg: string) => void): void;
168
190
  export declare function confirm(message: string): Promise<boolean>;
@@ -249,6 +249,33 @@ export function findMultiDocEntries(entries) {
249
249
  }
250
250
  return offenders;
251
251
  }
252
+ /**
253
+ * Return every `workspace` document in the push set, with the name it declares.
254
+ *
255
+ * A tree must carry at most ONE. The server applies the first workspace
256
+ * document it encounters to the workspace being pushed into and silently
257
+ * discards the rest, with no check that the document actually describes that
258
+ * workspace — so a second one renames the destination to a foreign name while
259
+ * leaving its content untouched, and which name wins flips with the contents of
260
+ * the pushed set.
261
+ *
262
+ * Trees accumulate a second one easily, because `pull` names the file after the
263
+ * workspace's own name (`workspace/{name}.xs`): pulling a different workspace
264
+ * into the same directory ADDS a file rather than overwriting, and renaming a
265
+ * workspace leaves the old-name file behind.
266
+ */
267
+ export function findWorkspaceEntries(entries) {
268
+ const found = [];
269
+ for (const entry of entries) {
270
+ const parsed = parseDocument(entry.content);
271
+ // Exact match: `workspace_trigger` is a different document type that also
272
+ // lives under workspace/ and must not be counted here.
273
+ if (parsed && parsed.type === 'workspace') {
274
+ found.push({ filePath: entry.filePath, name: parsed.name });
275
+ }
276
+ }
277
+ return found;
278
+ }
252
279
  // ── Validation Rendering ────────────────────────────────────────────────────
253
280
  export function renderBadReferences(badRefs, log) {
254
281
  log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
@@ -637,6 +664,33 @@ export async function executePush(ctx, target, flags) {
637
664
  command.error('After flattening, no .xs documents remain to push.');
638
665
  }
639
666
  }
667
+ // ── Refuse a tree carrying more than one workspace document ───────────
668
+ // The server applies the FIRST workspace document it sees to the workspace
669
+ // being pushed into and silently drops the rest, with no check that the
670
+ // document describes that workspace. A second one therefore renames the
671
+ // destination to a foreign name while leaving all of its content intact,
672
+ // and which name wins flips with whatever happens to be in the pushed set.
673
+ //
674
+ // Checked against the WHOLE tree rather than the partial changed-set, because
675
+ // the flip-flop case is exactly the one where only the foreign document is
676
+ // "changed" and would be the sole workspace document actually sent.
677
+ //
678
+ // No --force escape and no auto-fix: unlike the multi-doc bundle above there
679
+ // is no safe repair, since only the user knows which workspace this tree is
680
+ // meant to be. Picking one is the bug.
681
+ const workspaceDocs = findWorkspaceEntries(documentEntries);
682
+ if (workspaceDocs.length > 1) {
683
+ const list = workspaceDocs
684
+ .map((w) => ` ${relative(inputDir, w.filePath) || w.filePath} → workspace "${w.name}"`)
685
+ .join('\n');
686
+ command.error(`${inputDir} holds ${workspaceDocs.length} workspace documents:\n${list}\n\n` +
687
+ 'A push targets one workspace, and the server applies only the first of these and ' +
688
+ 'silently discards the rest — renaming the destination to a foreign name while leaving ' +
689
+ 'its content untouched.\n\n' +
690
+ 'Delete the ones that do not describe the workspace you are pushing to, keeping a single ' +
691
+ 'workspace/*.xs. (`pull` names this file after the workspace, so pulling a different ' +
692
+ 'workspace into this tree, or renaming one, leaves the old file behind.)');
693
+ }
640
694
  multidoc = documentEntries.map((d) => d.content).join('\n---\n');
641
695
  // ── Build document key → file path map (for GUID writeback) ─────────
642
696
  for (const entry of documentEntries) {