@shipdit/cli 0.4.0 → 0.5.0

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 +21 -0
  2. package/dist/index.mjs +340 -43
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -12,3 +12,24 @@ shipd types gen
12
12
  ```
13
13
 
14
14
  `shipd login` stores a scoped management API key. Use `SHIPD_API_KEY` for CI — keys carry org/project/environment metadata.
15
+
16
+ ## Go projects
17
+
18
+ The CLI reads `shipd.config.go` directly. Declare an exported
19
+ `var Config = config.ShipdConfig{...}` using
20
+ `github.com/alasti-company/useshipd.com/sdks/go/config`. Go 1.23+ and Node.js 22.12+
21
+ are needed for CLI commands; the application runtime only needs Go.
22
+
23
+ ```bash
24
+ shipd diff
25
+ shipd push
26
+ shipd types gen
27
+ shipd pull
28
+ ```
29
+
30
+ Go input automatically selects Go output in `internal/flags/flags_gen.go`.
31
+ Generation emits per-flag accessors, key and variant constants, and `Defaults()`.
32
+ Pull writes Go definitions back to `shipd.config.go`. No JSON flag file is needed.
33
+ The definition file is evaluated on its own inside the Go module: imports and
34
+ helpers declared in that file work; sibling files are not loaded. Do not define
35
+ `main` in it. See `examples/go-examples` for a complete example.
package/dist/index.mjs CHANGED
@@ -5,8 +5,30 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { z } from "zod";
7
7
  import { createJiti } from "jiti";
8
+ import { spawn } from "node:child_process";
8
9
 
10
+ //#region src/lib/errors.ts
11
+ var CliError = class extends Error {
12
+ code;
13
+ exitCode;
14
+ details;
15
+ constructor(code, message, options) {
16
+ super(message);
17
+ this.name = "CliError";
18
+ this.code = code;
19
+ this.exitCode = options?.exitCode ?? 1;
20
+ this.details = options?.details;
21
+ }
22
+ };
23
+ function exitCodeForError(error) {
24
+ if (error instanceof CliError) return error.exitCode;
25
+ return 1;
26
+ }
27
+
28
+ //#endregion
9
29
  //#region src/lib/config.ts
30
+ /** Hosted control plane. Used when no flag, env, project, global, or credential URL is set. */
31
+ const DEFAULT_API_URL = "https://api.useshipd.com";
10
32
  function readJsonFile(filePath) {
11
33
  if (!fs.existsSync(filePath)) return null;
12
34
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
@@ -42,7 +64,7 @@ function clearCredentials() {
42
64
  function findProjectRoot(start = process.cwd()) {
43
65
  let current = path.resolve(start);
44
66
  while (true) {
45
- if (fs.existsSync(path.join(current, "shipd.config.ts")) || fs.existsSync(path.join(current, ".shipd", "config.json"))) return current;
67
+ if (fs.existsSync(path.join(current, "shipd.config.ts")) || fs.existsSync(path.join(current, "shipd.config.go")) || fs.existsSync(path.join(current, "shipd.config.json")) || fs.existsSync(path.join(current, ".shipd", "config.json"))) return current;
46
68
  const parent = path.dirname(current);
47
69
  if (parent === current) return process.cwd();
48
70
  current = parent;
@@ -58,16 +80,25 @@ function resolveRuntimeConfig(flags, options) {
58
80
  const globalConfig = loadGlobalConfig();
59
81
  const credentials = loadCredentials();
60
82
  const ci = flags.ci === true || flags.yes === true || process.env.CI === "1" || process.env.CI === "true" || process.env.SHIPD_CI === "1";
61
- const defaultConfigFile = fs.existsSync(path.join(projectRoot, "shipd.config.ts")) ? path.join(projectRoot, "shipd.config.ts") : path.join(cwd, "shipd.config.ts");
83
+ const defaultConfigFile = path.join(projectRoot, [
84
+ "shipd.config.ts",
85
+ "shipd.config.go",
86
+ "shipd.config.json"
87
+ ].find((file) => fs.existsSync(path.join(projectRoot, file))) ?? "shipd.config.ts");
88
+ const configFile = flags.flagsConfig ? path.resolve(flags.flagsConfig) : projectConfig.configFile ?? defaultConfigFile;
89
+ const typesLanguage = projectConfig.typesLanguage ?? (path.extname(configFile) === ".go" ? "go" : "typescript");
90
+ if (typesLanguage !== "typescript" && typesLanguage !== "go") throw new CliError("validation_error", "typesLanguage must be typescript or go");
62
91
  return {
63
- apiUrl: flags.apiUrl ?? process.env.SHIPD_API_URL ?? projectConfig.apiUrl ?? globalConfig.apiUrl ?? credentials.apiUrl ?? options?.defaultApiUrl ?? "http://localhost:8000",
92
+ apiUrl: flags.apiUrl ?? process.env.SHIPD_API_URL ?? projectConfig.apiUrl ?? globalConfig.apiUrl ?? credentials.apiUrl ?? options?.defaultApiUrl ?? "https://api.useshipd.com",
64
93
  appUrl: process.env.SHIPD_APP_URL ?? projectConfig.appUrl ?? globalConfig.appUrl ?? "http://localhost:5173",
65
94
  apiKey: process.env.SHIPD_API_KEY ?? credentials.apiKey,
66
95
  org: flags.org ?? process.env.SHIPD_ORG ?? projectConfig.org ?? globalConfig.org,
67
96
  project: flags.project ?? process.env.SHIPD_PROJECT ?? projectConfig.project ?? globalConfig.project,
68
97
  env: flags.env ?? process.env.SHIPD_ENV ?? projectConfig.env ?? globalConfig.env,
69
- configFile: flags.flagsConfig ? path.resolve(flags.flagsConfig) : projectConfig.configFile ?? defaultConfigFile,
70
- typesOut: projectConfig.typesOut ?? "src/generated/flags.ts",
98
+ configFile,
99
+ typesOut: projectConfig.typesOut ?? (typesLanguage === "go" ? "internal/flags/flags_gen.go" : "src/generated/flags.ts"),
100
+ typesLanguage,
101
+ typesPackage: projectConfig.typesPackage ?? "flags",
71
102
  yes: ci,
72
103
  json: flags.json === true,
73
104
  cwd,
@@ -4198,25 +4229,6 @@ const f$1 = (e) => createAuthClient({
4198
4229
  plugins: d$1(e)
4199
4230
  });
4200
4231
 
4201
- //#endregion
4202
- //#region src/lib/errors.ts
4203
- var CliError = class extends Error {
4204
- code;
4205
- exitCode;
4206
- details;
4207
- constructor(code, message, options) {
4208
- super(message);
4209
- this.name = "CliError";
4210
- this.code = code;
4211
- this.exitCode = options?.exitCode ?? 1;
4212
- this.details = options?.details;
4213
- }
4214
- };
4215
- function exitCodeForError(error) {
4216
- if (error instanceof CliError) return error.exitCode;
4217
- return 1;
4218
- }
4219
-
4220
4232
  //#endregion
4221
4233
  //#region src/lib/client.ts
4222
4234
  const AUTH_BASE_PATH = "/api/v1/auth";
@@ -4384,7 +4396,6 @@ function e(e) {
4384
4396
  //#region src/commands/auth.ts
4385
4397
  const CLIENT_ID = "shipd-cli";
4386
4398
  const MANAGEMENT_KEY_PREFIX = "shipd_mgmt_";
4387
- const DEFAULT_API_URL = "https://api.useshipd.com";
4388
4399
  function registerAuthCommands(program) {
4389
4400
  program.command("login").description("Authenticate via browser device authorization").addOption(new Option("--api-url <url>", `API origin (default: ${DEFAULT_API_URL})`).env("SHIPD_API_URL")).action(async (_opts, command) => {
4390
4401
  const runtime = getRuntime(command, { defaultApiUrl: DEFAULT_API_URL });
@@ -4620,6 +4631,7 @@ const w = z.object({
4620
4631
  });
4621
4632
  });
4622
4633
  const T = z.object({
4634
+ expectedVersion: z.number().int().positive().optional(),
4623
4635
  flagId: z.string().min(1),
4624
4636
  key: o.optional(),
4625
4637
  name: z.string().min(1).optional(),
@@ -4632,6 +4644,7 @@ const E = z.object({ flagId: z.string().min(1) });
4632
4644
  const D = z.object({ projectId: z.string().min(1) });
4633
4645
  const O = z.object({ flagId: z.string().min(1) });
4634
4646
  const k = z.object({
4647
+ expectedVersion: z.number().int().positive().optional(),
4635
4648
  flagId: z.string().min(1),
4636
4649
  environmentId: z.string().min(1),
4637
4650
  enabled: z.boolean().optional(),
@@ -4639,6 +4652,7 @@ const k = z.object({
4639
4652
  fallthrough: u.optional()
4640
4653
  });
4641
4654
  const A = z.object({
4655
+ expectedVersion: z.number().int().positive().optional(),
4642
4656
  flagId: z.string().min(1),
4643
4657
  environmentId: z.string().min(1),
4644
4658
  enabled: z.boolean()
@@ -4784,6 +4798,188 @@ const J = z.object({
4784
4798
  approvalRequests: z.array(z.string()).default([])
4785
4799
  });
4786
4800
 
4801
+ //#endregion
4802
+ //#region src/lib/go-string.ts
4803
+ /** Go strings cannot represent JavaScript's unpaired UTF-16 surrogates. */
4804
+ function goString(value) {
4805
+ for (const character of value) {
4806
+ const code = character.charCodeAt(0);
4807
+ if (character.length === 1 && code >= 55296 && code <= 57343) throw new CliError("validation_error", `Cannot generate Go for a string containing an unpaired Unicode surrogate: ${JSON.stringify(value)}`);
4808
+ }
4809
+ return JSON.stringify(value);
4810
+ }
4811
+
4812
+ //#endregion
4813
+ //#region src/lib/go-config.ts
4814
+ /** Read the package clause after leading whitespace and comments, not comment text. */
4815
+ function goPackageClause(source) {
4816
+ let offset = 0;
4817
+ const skipTrivia = () => {
4818
+ while (offset < source.length) {
4819
+ const trivia = /^(?:\s+|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)/u.exec(source.slice(offset));
4820
+ if (!trivia) break;
4821
+ offset += trivia[0].length;
4822
+ }
4823
+ };
4824
+ skipTrivia();
4825
+ if (!/^package\b/u.test(source.slice(offset))) throw new Error("Go config must have a package declaration");
4826
+ offset += 7;
4827
+ skipTrivia();
4828
+ const name = /^[_\p{L}][_\p{L}\p{Nd}]*/u.exec(source.slice(offset))?.[0];
4829
+ if (!name) throw new Error("Go config must have a package name");
4830
+ return {
4831
+ name,
4832
+ start: offset,
4833
+ end: offset + name.length
4834
+ };
4835
+ }
4836
+ /** Evaluate a self-contained Go definition file within its own module. */
4837
+ async function loadGoConfig(file) {
4838
+ const source = fs.readFileSync(file, "utf8");
4839
+ const clause = goPackageClause(source);
4840
+ const directory = fs.mkdtempSync(path.join(path.dirname(file), ".shipd-go-"));
4841
+ const cleanup = () => fs.rmSync(directory, {
4842
+ recursive: true,
4843
+ force: true
4844
+ });
4845
+ process.once("exit", cleanup);
4846
+ let interrupted;
4847
+ let terminate;
4848
+ const interrupt = () => {
4849
+ interrupted = 130;
4850
+ terminate?.();
4851
+ };
4852
+ const shutdown = () => {
4853
+ interrupted = 143;
4854
+ terminate?.();
4855
+ };
4856
+ process.once("SIGINT", interrupt);
4857
+ process.once("SIGTERM", shutdown);
4858
+ try {
4859
+ fs.writeFileSync(path.join(directory, ".gitignore"), "*\n");
4860
+ fs.writeFileSync(path.join(directory, "definitions.go"), source.slice(0, clause.start) + "main" + source.slice(clause.end));
4861
+ fs.writeFileSync(path.join(directory, "main.go"), `package main
4862
+ import (
4863
+ "encoding/json"
4864
+ "fmt"
4865
+ "os"
4866
+ )
4867
+ func main() {
4868
+ file, err := os.Create(os.Args[1])
4869
+ if err == nil {
4870
+ err = json.NewEncoder(file).Encode(Config)
4871
+ if closeErr := file.Close(); err == nil { err = closeErr }
4872
+ }
4873
+ if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
4874
+ }
4875
+ `);
4876
+ const output = path.join(directory, "config.json");
4877
+ try {
4878
+ await new Promise((resolve, reject) => {
4879
+ const grouped = process.platform !== "win32";
4880
+ const child = spawn("go", [
4881
+ "run",
4882
+ ".",
4883
+ output
4884
+ ], {
4885
+ cwd: directory,
4886
+ detached: grouped,
4887
+ stdio: [
4888
+ "ignore",
4889
+ "ignore",
4890
+ "pipe"
4891
+ ]
4892
+ });
4893
+ let stderr = "";
4894
+ let failure;
4895
+ const kill = (signal) => {
4896
+ if (!child.pid) return;
4897
+ try {
4898
+ if (grouped) process.kill(-child.pid, signal);
4899
+ else child.kill(signal);
4900
+ } catch (error) {
4901
+ if (error.code !== "ESRCH") throw error;
4902
+ }
4903
+ };
4904
+ let killTimer;
4905
+ terminate = () => {
4906
+ kill("SIGTERM");
4907
+ killTimer = setTimeout(() => kill("SIGKILL"), 1e3);
4908
+ };
4909
+ const timer = setTimeout(() => {
4910
+ failure = /* @__PURE__ */ new Error("Go config evaluation timed out");
4911
+ kill("SIGKILL");
4912
+ }, 12e4);
4913
+ child.stderr.on("data", (chunk) => {
4914
+ stderr += chunk.toString();
4915
+ if (stderr.length > 8388608) {
4916
+ failure = /* @__PURE__ */ new Error("Go compiler output exceeds 8 MiB");
4917
+ kill("SIGKILL");
4918
+ }
4919
+ });
4920
+ child.once("error", (error) => {
4921
+ failure = error;
4922
+ });
4923
+ child.once("close", (code) => {
4924
+ clearTimeout(timer);
4925
+ clearTimeout(killTimer);
4926
+ terminate = void 0;
4927
+ if (failure || code !== 0) reject(Object.assign(failure ?? /* @__PURE__ */ new Error(`go run exited with code ${code}`), { stderr }));
4928
+ else resolve();
4929
+ });
4930
+ });
4931
+ } catch (error) {
4932
+ throw goRunError(error, path.basename(file));
4933
+ }
4934
+ return JSON.parse(fs.readFileSync(output, "utf8"));
4935
+ } finally {
4936
+ process.removeListener("SIGINT", interrupt);
4937
+ process.removeListener("SIGTERM", shutdown);
4938
+ process.removeListener("exit", cleanup);
4939
+ cleanup();
4940
+ if (interrupted !== void 0) process.exit(interrupted);
4941
+ }
4942
+ }
4943
+ function goRunError(error, configName) {
4944
+ if (error.code === "ENOENT") return /* @__PURE__ */ new Error("Go 1.23+ is required to load Go flag definitions, but `go` was not found on PATH. Install Go from https://go.dev/dl and try again.");
4945
+ const stderr = String(error.stderr ?? "").replace(/(?:\.\/)?definitions\.go/g, configName).trim();
4946
+ const hint = /\bundefined:/.test(stderr) ? `\n${configName} must be self-contained: the CLI compiles only this file, not sibling Go files. Move helper declarations into it or import them from another package.` : "";
4947
+ return new Error(stderr ? `go run failed:\n${stderr}${hint}` : String(error));
4948
+ }
4949
+ function goNumber(value) {
4950
+ const literal = JSON.stringify(value);
4951
+ return Number.isSafeInteger(value) || /[.eE]/.test(literal) ? literal : `${literal}.0`;
4952
+ }
4953
+ function goValue(value) {
4954
+ if (value === null) return "nil";
4955
+ if (typeof value === "number") return goNumber(value);
4956
+ if (typeof value === "string") return goString(value);
4957
+ if (Array.isArray(value)) return `[]any{${value.map(goValue).join(", ")}}`;
4958
+ if (typeof value === "object") return `map[string]any{${Object.entries(value).map(([key, entry]) => `${goString(key)}: ${goValue(entry)}`).join(", ")}}`;
4959
+ return JSON.stringify(value);
4960
+ }
4961
+ /** Emits gofmt-clean Go: one block per flag, field values aligned as gofmt does. */
4962
+ function generateGoConfigSource(flags, packageName = "main") {
4963
+ const entries = flags.map((flag) => {
4964
+ const fields = [["Type", goString(flag.type)], ["Default", goValue(flag.default)]];
4965
+ if (flag.variants) fields.push(["Variants", `[]string{${flag.variants.map((v) => goString(v)).join(", ")}}`]);
4966
+ if (flag.description !== void 0) fields.push(["Description", goString(flag.description)]);
4967
+ if (flag.tags.length) fields.push(["Tags", `[]string{${flag.tags.map((v) => goString(v)).join(", ")}}`]);
4968
+ if (flag.lifecycle === "deprecated") fields.push(["Deprecated", "true"]);
4969
+ if (flag.disabled) fields.push(["Disabled", "true"]);
4970
+ const width = Math.max(...fields.map(([name]) => name.length)) + 1;
4971
+ const lines = fields.map(([name, value]) => `\t\t${`${name}:`.padEnd(width)} ${value},`);
4972
+ return `\t${goString(flag.key)}: {\n${lines.join("\n")}\n\t},`;
4973
+ });
4974
+ return `// Generated by shipd pull. Pull replaces this file; review changes before committing.
4975
+ package ${packageName}
4976
+
4977
+ import "github.com/alasti-company/useshipd.com/sdks/go/config"
4978
+
4979
+ var Config = config.ShipdConfig${entries.length ? `{\n${entries.join("\n")}\n}` : "{}"}
4980
+ `;
4981
+ }
4982
+
4787
4983
  //#endregion
4788
4984
  //#region src/lib/local-config.ts
4789
4985
  const localFlagsArraySchema = z.array(R).superRefine((flags, ctx) => {
@@ -4800,23 +4996,32 @@ const localFlagsArraySchema = z.array(R).superRefine((flags, ctx) => {
4800
4996
  async function loadLocalFlags(configFile) {
4801
4997
  const absolute = path.resolve(configFile);
4802
4998
  if (!fs.existsSync(absolute)) throw new CliError("not_found", `Config file not found: ${absolute}`);
4803
- const jiti = createJiti(import.meta.url, { interopDefault: true });
4804
- let loaded;
4999
+ let config;
4805
5000
  try {
4806
- loaded = await jiti.import(absolute);
5001
+ if (path.extname(absolute) === ".json") {
5002
+ config = JSON.parse(fs.readFileSync(absolute, "utf8"));
5003
+ if (config?.version !== 1) throw new Error("JSON flag config must have version: 1");
5004
+ } else if (path.extname(absolute) === ".go") config = await loadGoConfig(absolute);
5005
+ else {
5006
+ const loaded = await createJiti(import.meta.url, { interopDefault: true }).import(absolute);
5007
+ config = loaded.default ?? loaded;
5008
+ }
4807
5009
  } catch (error) {
4808
5010
  const message = error instanceof Error ? error.message : String(error);
4809
5011
  throw new CliError("validation_error", `Failed to load config ${absolute}: ${message}`);
4810
5012
  }
4811
- const config = loaded.default ?? loaded;
4812
- if (!config.flags || typeof config.flags !== "object") throw new CliError("validation_error", "shipd.config.ts must export defineFlags(...) result");
5013
+ if (!config?.flags || typeof config.flags !== "object" || Array.isArray(config.flags)) throw new CliError("validation_error", "Flag config must contain a flags object (defineFlags result or version-1 JSON)");
5014
+ for (const [key, flag] of Object.entries(config.flags)) {
5015
+ if (!flag || typeof flag !== "object" || Array.isArray(flag)) throw new CliError("validation_error", `Invalid definition for flag ${JSON.stringify(key)}`);
5016
+ if ("deprecated" in flag && typeof flag.deprecated !== "boolean") throw new CliError("validation_error", `Flag ${JSON.stringify(key)} deprecated must be a boolean`);
5017
+ }
4813
5018
  const candidates = Object.entries(config.flags).map(([key, flag]) => ({
4814
5019
  key,
4815
5020
  type: flag.type,
4816
5021
  default: flag.default,
4817
5022
  description: flag.description,
4818
5023
  tags: flag.tags ?? [],
4819
- lifecycle: flag.lifecycle ?? "active",
5024
+ lifecycle: flag.lifecycle ?? (flag.deprecated === true ? "deprecated" : "active"),
4820
5025
  variants: flag.variants,
4821
5026
  disabled: flag.disabled
4822
5027
  }));
@@ -4903,6 +5108,14 @@ function serializeTypeLiteral(value) {
4903
5108
  if (value === null) return "null";
4904
5109
  return JSON.stringify(value);
4905
5110
  }
5111
+ /** Pull preserves JSON config workflows rather than introducing a TS dependency. */
5112
+ function generateJSONConfigSource(flags) {
5113
+ const entries = flags.map(({ key, ...definition }) => [key, definition]);
5114
+ return `${JSON.stringify({
5115
+ version: 1,
5116
+ flags: Object.fromEntries(entries)
5117
+ }, null, 2)}\n`;
5118
+ }
4906
5119
 
4907
5120
  //#endregion
4908
5121
  //#region src/lib/plan.ts
@@ -5165,24 +5378,100 @@ function registerProjectsCommands(program) {
5165
5378
  });
5166
5379
  }
5167
5380
 
5381
+ //#endregion
5382
+ //#region src/lib/go-typegen.ts
5383
+ const GO_KEYWORDS = new Set("break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var".split(" "));
5384
+ function goName(value) {
5385
+ let name = (value.normalize("NFC").match(/[\p{L}\p{Nd}]+/gu) ?? []).map((word) => word[0].toUpperCase() + word.slice(1)).join("");
5386
+ if (!/^\p{Lu}/u.test(name)) name = `Flag${name}`;
5387
+ return name;
5388
+ }
5389
+ function generateGoTypesSource(flags, packageName = "flags") {
5390
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(packageName) || packageName === "_" || GO_KEYWORDS.has(packageName)) throw new CliError("validation_error", `Invalid Go package name ${JSON.stringify(packageName)}`);
5391
+ const used = /* @__PURE__ */ new Set(["Defaults"]);
5392
+ const claim = (name, key) => {
5393
+ if (used.has(name)) throw new CliError("validation_error", `Go identifier ${name} collides for flag ${JSON.stringify(key)}; rename the flag or variant`);
5394
+ used.add(name);
5395
+ return name;
5396
+ };
5397
+ const declarations = flags.map((flag) => {
5398
+ const name = claim(goName(flag.key), flag.key);
5399
+ const keyName = claim(`${name}Key`, flag.key);
5400
+ const lines = [`const ${keyName} = ${goString(flag.key)}`];
5401
+ let returnType;
5402
+ let expression;
5403
+ switch (flag.type) {
5404
+ case "boolean":
5405
+ returnType = "bool";
5406
+ expression = `client.IsEnabled(${keyName}, context)`;
5407
+ break;
5408
+ case "number":
5409
+ returnType = "float64";
5410
+ expression = `client.GetNumber(${keyName}, context)`;
5411
+ break;
5412
+ case "json":
5413
+ returnType = "any";
5414
+ expression = `client.GetJSON(${keyName}, context)`;
5415
+ break;
5416
+ case "variant":
5417
+ returnType = claim(`${name}Variant`, flag.key);
5418
+ lines.push(`type ${returnType} string`);
5419
+ for (const variant of flag.variants ?? []) {
5420
+ const constant = claim(`${name}${goName(variant)}`, flag.key);
5421
+ lines.push(`const ${constant} ${returnType} = ${goString(variant)}`);
5422
+ }
5423
+ expression = `${returnType}(client.GetVariant(${keyName}, context))`;
5424
+ }
5425
+ lines.push(`func ${name}(client *shipd.Client, context shipd.EvaluationContext) ${returnType} {\n\treturn ${expression}\n}`);
5426
+ return lines.join("\n\n");
5427
+ }).join("\n\n");
5428
+ const defaults = Object.fromEntries(flags.map((flag) => [flag.key, flag.default]));
5429
+ return `// Code generated by shipd types gen --language go; DO NOT EDIT.
5430
+ package ${packageName}
5431
+
5432
+ import (
5433
+ \t"encoding/json"
5434
+ ${flags.length ? " shipd \"github.com/alasti-company/useshipd.com/sdks/go\"\n" : ""})
5435
+
5436
+ // Defaults returns a fresh map of the config's defaults for shipd.Options.
5437
+ func Defaults() map[string]any {
5438
+ \tvar defaults map[string]any
5439
+ \t_ = json.Unmarshal([]byte(${JSON.stringify(JSON.stringify(defaults))}), &defaults)
5440
+ \treturn defaults
5441
+ }
5442
+
5443
+ ${declarations}
5444
+ `.trimEnd() + "\n";
5445
+ }
5446
+
5168
5447
  //#endregion
5169
5448
  //#region src/lib/typegen.ts
5170
- async function runTypegen(runtime, outPath = runtime.typesOut) {
5171
- const flags = await loadLocalFlags(runtime.configFile);
5449
+ async function runTypegen(runtime, outPath = runtime.typesOut, options = {}) {
5450
+ return writeTypegen(prepareTypegen(runtime, await loadLocalFlags(runtime.configFile), outPath, options));
5451
+ }
5452
+ function prepareTypegen(runtime, flags, outPath = runtime.typesOut, options = {}) {
5172
5453
  const absolute = path.resolve(outPath);
5173
- const source = generateTypesSource(flags);
5174
- fs.mkdirSync(path.dirname(absolute), { recursive: true });
5175
- fs.writeFileSync(absolute, source);
5454
+ const language = options.language ?? runtime.typesLanguage ?? "typescript";
5455
+ if (language !== "go" && language !== "typescript") throw new CliError("validation_error", "Type generation language must be typescript or go");
5176
5456
  return {
5457
+ source: language === "go" ? generateGoTypesSource(flags, options.packageName ?? runtime.typesPackage ?? "flags") : generateTypesSource(flags),
5177
5458
  written: absolute,
5178
5459
  flagCount: flags.length
5179
5460
  };
5180
5461
  }
5462
+ function writeTypegen(prepared) {
5463
+ fs.mkdirSync(path.dirname(prepared.written), { recursive: true });
5464
+ fs.writeFileSync(prepared.written, prepared.source);
5465
+ return {
5466
+ written: prepared.written,
5467
+ flagCount: prepared.flagCount
5468
+ };
5469
+ }
5181
5470
 
5182
5471
  //#endregion
5183
5472
  //#region src/commands/pull.ts
5184
5473
  function registerPullCommand(program) {
5185
- program.command("pull").description("Regenerate shipd.config.ts from the server and run typegen").action(async (_opts, command) => {
5474
+ program.command("pull").description("Regenerate the flag config from the server and run typegen").action(async (_opts, command) => {
5186
5475
  const runtime = getRuntime(command);
5187
5476
  const client = await createCliApiClient(runtime);
5188
5477
  const { projectId, environmentId } = await resolveIds(client, runtime);
@@ -5199,11 +5488,13 @@ function registerPullCommand(program) {
5199
5488
  variants: flag.variants,
5200
5489
  disabled: flag.disabled
5201
5490
  }));
5202
- const source = generateShipdConfigSource(localFlags);
5491
+ const packageName = path.extname(runtime.configFile) === ".go" && fs.existsSync(runtime.configFile) ? goPackageClause(fs.readFileSync(runtime.configFile, "utf8")).name : "main";
5492
+ const source = path.extname(runtime.configFile) === ".json" ? generateJSONConfigSource(localFlags) : path.extname(runtime.configFile) === ".go" ? generateGoConfigSource(localFlags, packageName) : generateShipdConfigSource(localFlags);
5493
+ const preparedTypes = prepareTypegen(runtime, localFlags);
5203
5494
  const outPath = path.resolve(runtime.configFile);
5204
5495
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
5205
5496
  fs.writeFileSync(outPath, source);
5206
- const types = await runTypegen(runtime);
5497
+ const types = writeTypegen(preparedTypes);
5207
5498
  printResult(runtime, {
5208
5499
  written: outPath,
5209
5500
  flagCount: localFlags.length,
@@ -5258,6 +5549,7 @@ function registerPushCommand(program) {
5258
5549
  }
5259
5550
  if (planHasDefinitionChanges(plan) && !runtime.json) console.error("Note: definition/ownership changes apply to all environments (requires production permission).");
5260
5551
  if (!await confirm(runtime, "Apply this plan?")) throw new CliError("validation_error", "Push cancelled (pass --yes or --ci to apply non-interactively)");
5552
+ const preparedTypes = prepareTypegen(runtime, localFlags);
5261
5553
  const expectedVersions = expectedVersionsFromChanges(planned.changes);
5262
5554
  const applied = await client.flag.push({
5263
5555
  projectId,
@@ -5270,7 +5562,7 @@ function registerPushCommand(program) {
5270
5562
  expectedVersions
5271
5563
  }).catch(mapApiError);
5272
5564
  const appliedPlan = buildSyncPlan(applied.changes);
5273
- const types = await runTypegen(runtime);
5565
+ const types = writeTypegen(preparedTypes);
5274
5566
  const approvalRequests = applied.approvalRequests ?? [];
5275
5567
  printResult(runtime, {
5276
5568
  plan: appliedPlan,
@@ -5365,9 +5657,14 @@ function registerSnapshotsCommands(program) {
5365
5657
  //#endregion
5366
5658
  //#region src/commands/types.ts
5367
5659
  function registerTypesCommands(program) {
5368
- program.command("types").description("Type generation helpers").command("gen").description("Generate TypeScript types from shipd.config.ts").option("-o, --out <path>", "Output file path").action(async (opts, command) => {
5660
+ program.command("types").description("Type generation helpers").command("gen").description("Generate TypeScript types or Go accessors from the local flag config").addOption(new Option("--language <language>", "Output language").choices(["typescript", "go"])).option("--package <name>", "Go package name (default: flags)").option("-o, --out <path>", "Output file path").action(async (opts, command) => {
5369
5661
  const runtime = getRuntime(command);
5370
- const result = await runTypegen(runtime, opts.out ?? runtime.typesOut);
5662
+ const language = opts.language ?? runtime.typesLanguage ?? "typescript";
5663
+ const defaultOutput = language === (runtime.typesLanguage ?? "typescript") ? runtime.typesOut : language === "go" ? "internal/flags/flags_gen.go" : "src/generated/flags.ts";
5664
+ const result = await runTypegen(runtime, opts.out ?? defaultOutput, {
5665
+ language,
5666
+ packageName: opts.package
5667
+ });
5371
5668
  printResult(runtime, result, `Wrote types for ${result.flagCount} flag(s) to ${result.written}`);
5372
5669
  });
5373
5670
  }
@@ -5376,7 +5673,7 @@ function registerTypesCommands(program) {
5376
5673
  //#region src/index.ts
5377
5674
  /** Detected before parse so usage errors can suppress Commander text under `--json`. */
5378
5675
  const jsonRequested = process.argv.includes("--json");
5379
- const program = new Command().name("shipd").description("shipd CLI — flags as code").option("--json", "Machine-readable JSON output", false).option("--yes", "Skip confirmation prompts", false).option("--ci", "Non-interactive mode (same as --yes / CI=1)", false).option("--api-url <url>", "API origin").option("--org <slug>", "Organization slug (session: select; API key: must match)").option("--project <slug>", "Project slug").option("--env <slug>", "Environment slug").option("--config <path>", "Project settings JSON (.shipd/config.json shape)").option("--flags-config <path>", "Path to shipd.config.ts").exitOverride().showHelpAfterError(!jsonRequested);
5676
+ const program = new Command().name("shipd").description("shipd CLI — flags as code").option("--json", "Machine-readable JSON output", false).option("--yes", "Skip confirmation prompts", false).option("--ci", "Non-interactive mode (same as --yes / CI=1)", false).option("--api-url <url>", `API origin (default: ${DEFAULT_API_URL})`).option("--org <slug>", "Organization slug (session: select; API key: must match)").option("--project <slug>", "Project slug").option("--env <slug>", "Environment slug").option("--config <path>", "Project settings JSON (.shipd/config.json shape)").option("--flags-config <path>", "Path to shipd.config.ts, shipd.config.go, or shipd.config.json").exitOverride().showHelpAfterError(!jsonRequested);
5380
5677
  if (jsonRequested) program.configureOutput({
5381
5678
  writeErr: () => {},
5382
5679
  writeOut: () => {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipdit/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "shipd's command-line interface",
5
5
  "homepage": "https://docs.useshipd.com",
6
6
  "bugs": {
@@ -18,7 +18,7 @@
18
18
  "dependencies": {
19
19
  "commander": "^15.0.0",
20
20
  "jiti": "^2.5.1",
21
- "shipdit": "0.3.1",
21
+ "shipdit": "0.4.0",
22
22
  "zod": "^4.6.5"
23
23
  },
24
24
  "devDependencies": {