@groundfloorcloud/cli 0.1.10 → 0.1.12

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.
Files changed (3) hide show
  1. package/README.md +26 -4
  2. package/dist/index.js +259 -0
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # `@groundfloorcloud/cli` (`gf`)
2
2
 
3
3
  Sign in to Groundfloor, manage workspaces, and work with Coderunner,
4
- secrets, files, apps, and Dataplane from your terminal.
4
+ secrets, feature flags, files, apps, and Dataplane from your terminal.
5
5
 
6
6
  `gf login` opens a browser, you sign in, and the session is cached and
7
7
  refreshed automatically.
@@ -226,6 +226,27 @@ gf apps ls
226
226
  gf apps get <id|slug>
227
227
  ```
228
228
 
229
+ ## Feature flags
230
+
231
+ Per-environment toggles and config values. `-e` takes an environment id or name
232
+ (`dev`, `staging`, `prod`); without it the workspace default (`dev`) is used.
233
+
234
+ ```bash
235
+ gf flags ls -e prod
236
+ gf flags set new-checkout true -e dev # boolean (inferred)
237
+ gf flags set max-items 25 -e dev # number (inferred)
238
+ gf flags set banner '{"text":"Hi"}' --type json -e dev
239
+ gf flags get new-checkout -e prod
240
+ gf flags compare dev prod
241
+ gf flags promote dev prod --dry-run # then without --dry-run
242
+ gf flags rm new-checkout -e dev
243
+ ```
244
+
245
+ Keys are lowercase letters, digits, `-`, `_` and `.`, starting with a letter.
246
+ Apps read flags at runtime from `GET /v1/workspaces/{id}/flags/runtime`: Shell
247
+ apps with the end-user token, Coderunners with `gf m2m token` (read-only). See
248
+ https://docs.groundfloor.cloud/docs/configure/feature-flags.
249
+
229
250
  ## Commands
230
251
 
231
252
  | Command | Description |
@@ -247,6 +268,7 @@ gf apps get <id|slug>
247
268
  | `gf deploy` | Package code and deploy as a coderunner. |
248
269
  | `gf domains ls\|add\|rm\|verify -c <id\|slug>` | Custom domains for a service. |
249
270
  | `gf secrets ls\|get\|set\|rm` | Workspace secrets. |
271
+ | `gf flags ls\|get\|set\|rm\|compare\|promote` | Feature flags per environment (`-e <id\|name>`). |
250
272
  | `gf files ls\|upload\|download\|rm` | Workspace files. |
251
273
  | `gf apps init --slug <slug>` | Download the official Shell starter-kit and stamp APP_ID. |
252
274
  | `gf apps ls\|get\|create\|publish` | Product apps (Shell publish / standalone wrap). |
@@ -287,7 +309,7 @@ sign-in fails with an invalid redirect URI:
287
309
  ## More
288
310
 
289
311
  - [Customer Portal](https://app.groundfloor.cloud)
290
- - [Developers](https://docs.groundfloor.cloud/docs/customer-portal/developers)
291
- - [Deploy with agents](https://docs.groundfloor.cloud/docs/customer-portal/agents)
292
- - [Coderunner](https://docs.groundfloor.cloud/docs/customer-portal/coderunner)
312
+ - [Developers](https://docs.groundfloor.cloud/docs/developer-tools/cli)
313
+ - [Deploy with agents](https://docs.groundfloor.cloud/docs/developer-tools/ai-agents)
314
+ - [Coderunner](https://docs.groundfloor.cloud/docs/deploy)
293
315
  - [API authentication](https://docs.groundfloor.cloud/docs/api/authentication)
package/dist/index.js CHANGED
@@ -850,6 +850,53 @@ function deleteSecret(apiUrl, workspaceId, key, environmentId) {
850
850
  })}`
851
851
  );
852
852
  }
853
+ function listFlags(apiUrl, workspaceId, environmentId) {
854
+ return cpGet(
855
+ apiUrl,
856
+ `${wsPath(workspaceId, "/flags")}${queryString({ environment_id: environmentId })}`
857
+ );
858
+ }
859
+ function getFlag(apiUrl, workspaceId, key, environmentId) {
860
+ return cpGet(
861
+ apiUrl,
862
+ `${wsPath(workspaceId, `/flags/${encodeURIComponent(key)}`)}${queryString({
863
+ environment_id: environmentId
864
+ })}`
865
+ );
866
+ }
867
+ function putFlag(apiUrl, workspaceId, key, payload, environmentId) {
868
+ return cpPutJson(
869
+ apiUrl,
870
+ `${wsPath(workspaceId, `/flags/${encodeURIComponent(key)}`)}${queryString({
871
+ environment_id: environmentId
872
+ })}`,
873
+ payload
874
+ );
875
+ }
876
+ function deleteFlag(apiUrl, workspaceId, key, environmentId) {
877
+ return cpDelete(
878
+ apiUrl,
879
+ `${wsPath(workspaceId, `/flags/${encodeURIComponent(key)}`)}${queryString({
880
+ environment_id: environmentId
881
+ })}`
882
+ );
883
+ }
884
+ function compareFlags(apiUrl, workspaceId, sourceEnvironmentId, targetEnvironmentId) {
885
+ return cpGet(
886
+ apiUrl,
887
+ `${wsPath(workspaceId, "/flags/compare")}${queryString({
888
+ source_environment_id: sourceEnvironmentId,
889
+ target_environment_id: targetEnvironmentId
890
+ })}`
891
+ );
892
+ }
893
+ function promoteFlags(apiUrl, workspaceId, body) {
894
+ return cpPostJson(
895
+ apiUrl,
896
+ wsPath(workspaceId, "/flags/promote"),
897
+ body
898
+ );
899
+ }
853
900
  function listFiles(apiUrl, workspaceId) {
854
901
  return cpGet(apiUrl, wsPath(workspaceId, "/files"));
855
902
  }
@@ -2395,6 +2442,200 @@ async function secretsRmCommand(key, opts) {
2395
2442
  `);
2396
2443
  }
2397
2444
 
2445
+ // src/commands/flags.ts
2446
+ var VALUE_TYPES = [
2447
+ "boolean",
2448
+ "string",
2449
+ "number",
2450
+ "json"
2451
+ ];
2452
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2453
+ function matchEnvironment(environments2, idOrName) {
2454
+ const wanted = idOrName.trim();
2455
+ const byId = environments2.find((env) => env.id === wanted);
2456
+ if (byId) return byId.id;
2457
+ const byName = environments2.filter(
2458
+ (env) => env.name.toLowerCase() === wanted.toLowerCase()
2459
+ );
2460
+ if (byName.length === 1) return byName[0].id;
2461
+ const names2 = environments2.map((env) => env.name).join(", ") || "none";
2462
+ throw new Error(
2463
+ `Environment "${wanted}" not found in this workspace (have: ${names2}). Run \`gf environments ls\`.`
2464
+ );
2465
+ }
2466
+ async function resolveEnvironment(apiUrl, workspaceId, idOrName) {
2467
+ if (!idOrName) return void 0;
2468
+ if (UUID_RE2.test(idOrName.trim())) return idOrName.trim();
2469
+ const environments2 = await listWorkspaceEnvironments(apiUrl, workspaceId);
2470
+ return matchEnvironment(environments2, idOrName);
2471
+ }
2472
+ function parseFlagValue(raw, type) {
2473
+ if (type !== void 0 && !VALUE_TYPES.includes(type)) {
2474
+ throw new Error(`--type must be one of: ${VALUE_TYPES.join(", ")}`);
2475
+ }
2476
+ const lower = raw.trim().toLowerCase();
2477
+ const valueType = type ?? (lower === "true" || lower === "false" ? "boolean" : raw.trim() !== "" && Number.isFinite(Number(raw)) ? "number" : "string");
2478
+ switch (valueType) {
2479
+ case "boolean":
2480
+ if (lower !== "true" && lower !== "false") {
2481
+ throw new Error("boolean flags take true or false");
2482
+ }
2483
+ return { value: lower === "true", value_type: "boolean" };
2484
+ case "number": {
2485
+ const n = Number(raw);
2486
+ if (raw.trim() === "" || !Number.isFinite(n)) {
2487
+ throw new Error(`"${raw}" is not a number`);
2488
+ }
2489
+ return { value: n, value_type: "number" };
2490
+ }
2491
+ case "json":
2492
+ try {
2493
+ return { value: JSON.parse(raw), value_type: "json" };
2494
+ } catch {
2495
+ throw new Error("--type json needs a valid JSON value");
2496
+ }
2497
+ default:
2498
+ return { value: raw, value_type: "string" };
2499
+ }
2500
+ }
2501
+ function formatValue(value) {
2502
+ return typeof value === "string" ? value : JSON.stringify(value);
2503
+ }
2504
+ async function flagsListCommand(opts) {
2505
+ const cfg = await requireWorkspace(opts.workspace);
2506
+ const env = await resolveEnvironment(
2507
+ cfg.apiUrl,
2508
+ cfg.workspaceId,
2509
+ opts.environment
2510
+ );
2511
+ const data = await listFlags(cfg.apiUrl, cfg.workspaceId, env);
2512
+ if (opts.json) {
2513
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
2514
+ `);
2515
+ return;
2516
+ }
2517
+ process.stderr.write(`environment ${data.environment_id}
2518
+ `);
2519
+ if (data.flags.length === 0) {
2520
+ process.stdout.write("No flags in this environment.\n");
2521
+ return;
2522
+ }
2523
+ for (const flag of data.flags) {
2524
+ process.stdout.write(
2525
+ `${flag.key} ${flag.value_type} ${formatValue(flag.value)}
2526
+ `
2527
+ );
2528
+ }
2529
+ }
2530
+ async function flagsGetCommand(key, opts) {
2531
+ const cfg = await requireWorkspace(opts.workspace);
2532
+ const env = await resolveEnvironment(
2533
+ cfg.apiUrl,
2534
+ cfg.workspaceId,
2535
+ opts.environment
2536
+ );
2537
+ const flag = await getFlag(cfg.apiUrl, cfg.workspaceId, key, env);
2538
+ if (opts.json) {
2539
+ process.stdout.write(`${JSON.stringify(flag, null, 2)}
2540
+ `);
2541
+ return;
2542
+ }
2543
+ process.stdout.write(`${formatValue(flag.value)}
2544
+ `);
2545
+ }
2546
+ async function flagsSetCommand(key, value, opts) {
2547
+ const cfg = await requireWorkspace(opts.workspace);
2548
+ const env = await resolveEnvironment(
2549
+ cfg.apiUrl,
2550
+ cfg.workspaceId,
2551
+ opts.environment
2552
+ );
2553
+ const parsed = parseFlagValue(value, opts.type);
2554
+ const saved = await putFlag(
2555
+ cfg.apiUrl,
2556
+ cfg.workspaceId,
2557
+ key,
2558
+ { ...parsed, description: opts.description },
2559
+ env
2560
+ );
2561
+ if (opts.json) {
2562
+ process.stdout.write(`${JSON.stringify(saved, null, 2)}
2563
+ `);
2564
+ return;
2565
+ }
2566
+ process.stdout.write(
2567
+ `Set ${saved.key} = ${formatValue(saved.value)} (${saved.value_type}) in environment ${saved.environment_id}
2568
+ `
2569
+ );
2570
+ }
2571
+ async function flagsRmCommand(key, opts) {
2572
+ const cfg = await requireWorkspace(opts.workspace);
2573
+ const env = await resolveEnvironment(
2574
+ cfg.apiUrl,
2575
+ cfg.workspaceId,
2576
+ opts.environment
2577
+ );
2578
+ await deleteFlag(cfg.apiUrl, cfg.workspaceId, key, env);
2579
+ process.stdout.write(`Deleted flag ${key}
2580
+ `);
2581
+ }
2582
+ async function flagsCompareCommand(source, target, opts) {
2583
+ const cfg = await requireWorkspace(opts.workspace);
2584
+ const [src, tgt] = await Promise.all([
2585
+ resolveEnvironment(cfg.apiUrl, cfg.workspaceId, source),
2586
+ resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
2587
+ ]);
2588
+ const data = await compareFlags(cfg.apiUrl, cfg.workspaceId, src, tgt);
2589
+ if (opts.json) {
2590
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
2591
+ `);
2592
+ return;
2593
+ }
2594
+ if (data.keys.length === 0) {
2595
+ process.stdout.write("No flags in either environment.\n");
2596
+ return;
2597
+ }
2598
+ for (const row of data.keys) {
2599
+ process.stdout.write(
2600
+ `${row.key} ${row.status} ${formatValue(row.source_value ?? null)} \u2192 ${formatValue(row.target_value ?? null)}
2601
+ `
2602
+ );
2603
+ }
2604
+ }
2605
+ async function flagsPromoteCommand(source, target, opts) {
2606
+ const cfg = await requireWorkspace(opts.workspace);
2607
+ const [src, tgt] = await Promise.all([
2608
+ resolveEnvironment(cfg.apiUrl, cfg.workspaceId, source),
2609
+ resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
2610
+ ]);
2611
+ const keys = opts.keys?.split(",").map((k) => k.trim()).filter(Boolean);
2612
+ const result = await promoteFlags(cfg.apiUrl, cfg.workspaceId, {
2613
+ source_environment_id: src,
2614
+ target_environment_id: tgt,
2615
+ keys: keys && keys.length > 0 ? keys : void 0,
2616
+ mode: opts.overwrite ? "overwrite" : "skip_existing",
2617
+ dry_run: Boolean(opts.dryRun)
2618
+ });
2619
+ if (opts.json) {
2620
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
2621
+ `);
2622
+ return;
2623
+ }
2624
+ const prefix = result.dry_run ? "[dry run] " : "";
2625
+ process.stdout.write(
2626
+ `${prefix}copied: ${result.copied.join(", ") || "none"}
2627
+ ${prefix}skipped (already set; use --overwrite): ${result.skipped.join(", ") || "none"}
2628
+ `
2629
+ );
2630
+ for (const failure of result.failed) {
2631
+ process.stdout.write(`${prefix}failed: ${failure.key} \u2014 ${failure.error}
2632
+ `);
2633
+ }
2634
+ if (result.failed.length > 0) {
2635
+ process.exitCode = 1;
2636
+ }
2637
+ }
2638
+
2398
2639
  // src/commands/files.ts
2399
2640
  import { promises as fs5 } from "fs";
2400
2641
  import path6 from "path";
@@ -3459,6 +3700,24 @@ secrets.command("set <key> [value]").description("Create or update a secret (pas
3459
3700
  (key, value, opts) => run(() => secretsSetCommand(key, value, opts))
3460
3701
  );
3461
3702
  secrets.command("rm <key>").alias("delete").description("Delete a secret").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <id>", "Environment id").action((key, opts) => run(() => secretsRmCommand(key, opts)));
3703
+ var flags = program.command("flags").description(
3704
+ "Manage feature flags per environment (apps read them via GET \u2026/flags/runtime)"
3705
+ );
3706
+ var ENV_HELP = "Environment id or name (dev, staging, prod). Default: workspace default (dev)";
3707
+ flags.command("ls").alias("list").description("List flags and values in an environment").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <idOrName>", ENV_HELP).option("--json", "Output raw JSON").action((opts) => run(() => flagsListCommand(opts)));
3708
+ flags.command("get <key>").description("Print a flag value").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <idOrName>", ENV_HELP).option("--json", "Output raw JSON").action((key, opts) => run(() => flagsGetCommand(key, opts)));
3709
+ flags.command("set <key> <value>").description(
3710
+ "Create or update a flag (type inferred: true/false \u2192 boolean, numeric \u2192 number, else string)"
3711
+ ).option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <idOrName>", ENV_HELP).option("-t, --type <type>", "boolean | string | number | json").option("--description <text>", "Optional description (max 255 chars)").option("--json", "Output raw JSON").action(
3712
+ (key, value, opts) => run(() => flagsSetCommand(key, value, opts))
3713
+ );
3714
+ flags.command("rm <key>").alias("delete").description("Delete a flag from one environment").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <idOrName>", ENV_HELP).action((key, opts) => run(() => flagsRmCommand(key, opts)));
3715
+ flags.command("compare <source> <target>").alias("diff").description("Diff flags between two environments (ids or names)").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action(
3716
+ (source, target, opts) => run(() => flagsCompareCommand(source, target, opts))
3717
+ );
3718
+ flags.command("promote <source> <target>").description("Copy flags from one environment to another").option("-w, --workspace <id>", "Workspace id override").option("-k, --keys <keys>", "Comma-separated keys (default: all)").option("--overwrite", "Replace values already set in the target").option("--dry-run", "Show what would change without writing").option("--json", "Output raw JSON").action(
3719
+ (source, target, opts) => run(() => flagsPromoteCommand(source, target, opts))
3720
+ );
3462
3721
  var files = program.command("files").description("Manage workspace files");
3463
3722
  files.command("ls").alias("list").description("List files").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => filesListCommand(opts)));
3464
3723
  files.command("upload <path>").description("Upload a local file").option("-w, --workspace <id>", "Workspace id override").option("-n, --name <name>", "Display name (default: basename)").action(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundfloorcloud/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Groundfloor gf CLI — sign in, deploy coderunners, publish Shell apps, and manage secrets, files, and Dataplane.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "url": "git+ssh://git@bitbucket.org/codementeam/groundfloor-client-portal.git",
19
19
  "directory": "packages/cli"
20
20
  },
21
- "homepage": "https://docs.groundfloor.cloud/docs/customer-portal/developers",
21
+ "homepage": "https://docs.groundfloor.cloud/docs/developer-tools/cli",
22
22
  "keywords": [
23
23
  "groundfloor",
24
24
  "cli",