@shipdit/cli 0.4.1 → 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 +337 -41
  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,7 +5,27 @@ 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
10
30
  /** Hosted control plane. Used when no flag, env, project, global, or credential URL is set. */
11
31
  const DEFAULT_API_URL = "https://api.useshipd.com";
@@ -44,7 +64,7 @@ function clearCredentials() {
44
64
  function findProjectRoot(start = process.cwd()) {
45
65
  let current = path.resolve(start);
46
66
  while (true) {
47
- 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;
48
68
  const parent = path.dirname(current);
49
69
  if (parent === current) return process.cwd();
50
70
  current = parent;
@@ -60,7 +80,14 @@ function resolveRuntimeConfig(flags, options) {
60
80
  const globalConfig = loadGlobalConfig();
61
81
  const credentials = loadCredentials();
62
82
  const ci = flags.ci === true || flags.yes === true || process.env.CI === "1" || process.env.CI === "true" || process.env.SHIPD_CI === "1";
63
- 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");
64
91
  return {
65
92
  apiUrl: flags.apiUrl ?? process.env.SHIPD_API_URL ?? projectConfig.apiUrl ?? globalConfig.apiUrl ?? credentials.apiUrl ?? options?.defaultApiUrl ?? "https://api.useshipd.com",
66
93
  appUrl: process.env.SHIPD_APP_URL ?? projectConfig.appUrl ?? globalConfig.appUrl ?? "http://localhost:5173",
@@ -68,8 +95,10 @@ function resolveRuntimeConfig(flags, options) {
68
95
  org: flags.org ?? process.env.SHIPD_ORG ?? projectConfig.org ?? globalConfig.org,
69
96
  project: flags.project ?? process.env.SHIPD_PROJECT ?? projectConfig.project ?? globalConfig.project,
70
97
  env: flags.env ?? process.env.SHIPD_ENV ?? projectConfig.env ?? globalConfig.env,
71
- configFile: flags.flagsConfig ? path.resolve(flags.flagsConfig) : projectConfig.configFile ?? defaultConfigFile,
72
- 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",
73
102
  yes: ci,
74
103
  json: flags.json === true,
75
104
  cwd,
@@ -4200,25 +4229,6 @@ const f$1 = (e) => createAuthClient({
4200
4229
  plugins: d$1(e)
4201
4230
  });
4202
4231
 
4203
- //#endregion
4204
- //#region src/lib/errors.ts
4205
- var CliError = class extends Error {
4206
- code;
4207
- exitCode;
4208
- details;
4209
- constructor(code, message, options) {
4210
- super(message);
4211
- this.name = "CliError";
4212
- this.code = code;
4213
- this.exitCode = options?.exitCode ?? 1;
4214
- this.details = options?.details;
4215
- }
4216
- };
4217
- function exitCodeForError(error) {
4218
- if (error instanceof CliError) return error.exitCode;
4219
- return 1;
4220
- }
4221
-
4222
4232
  //#endregion
4223
4233
  //#region src/lib/client.ts
4224
4234
  const AUTH_BASE_PATH = "/api/v1/auth";
@@ -4621,6 +4631,7 @@ const w = z.object({
4621
4631
  });
4622
4632
  });
4623
4633
  const T = z.object({
4634
+ expectedVersion: z.number().int().positive().optional(),
4624
4635
  flagId: z.string().min(1),
4625
4636
  key: o.optional(),
4626
4637
  name: z.string().min(1).optional(),
@@ -4633,6 +4644,7 @@ const E = z.object({ flagId: z.string().min(1) });
4633
4644
  const D = z.object({ projectId: z.string().min(1) });
4634
4645
  const O = z.object({ flagId: z.string().min(1) });
4635
4646
  const k = z.object({
4647
+ expectedVersion: z.number().int().positive().optional(),
4636
4648
  flagId: z.string().min(1),
4637
4649
  environmentId: z.string().min(1),
4638
4650
  enabled: z.boolean().optional(),
@@ -4640,6 +4652,7 @@ const k = z.object({
4640
4652
  fallthrough: u.optional()
4641
4653
  });
4642
4654
  const A = z.object({
4655
+ expectedVersion: z.number().int().positive().optional(),
4643
4656
  flagId: z.string().min(1),
4644
4657
  environmentId: z.string().min(1),
4645
4658
  enabled: z.boolean()
@@ -4785,6 +4798,188 @@ const J = z.object({
4785
4798
  approvalRequests: z.array(z.string()).default([])
4786
4799
  });
4787
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
+
4788
4983
  //#endregion
4789
4984
  //#region src/lib/local-config.ts
4790
4985
  const localFlagsArraySchema = z.array(R).superRefine((flags, ctx) => {
@@ -4801,23 +4996,32 @@ const localFlagsArraySchema = z.array(R).superRefine((flags, ctx) => {
4801
4996
  async function loadLocalFlags(configFile) {
4802
4997
  const absolute = path.resolve(configFile);
4803
4998
  if (!fs.existsSync(absolute)) throw new CliError("not_found", `Config file not found: ${absolute}`);
4804
- const jiti = createJiti(import.meta.url, { interopDefault: true });
4805
- let loaded;
4999
+ let config;
4806
5000
  try {
4807
- 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
+ }
4808
5009
  } catch (error) {
4809
5010
  const message = error instanceof Error ? error.message : String(error);
4810
5011
  throw new CliError("validation_error", `Failed to load config ${absolute}: ${message}`);
4811
5012
  }
4812
- const config = loaded.default ?? loaded;
4813
- 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
+ }
4814
5018
  const candidates = Object.entries(config.flags).map(([key, flag]) => ({
4815
5019
  key,
4816
5020
  type: flag.type,
4817
5021
  default: flag.default,
4818
5022
  description: flag.description,
4819
5023
  tags: flag.tags ?? [],
4820
- lifecycle: flag.lifecycle ?? "active",
5024
+ lifecycle: flag.lifecycle ?? (flag.deprecated === true ? "deprecated" : "active"),
4821
5025
  variants: flag.variants,
4822
5026
  disabled: flag.disabled
4823
5027
  }));
@@ -4904,6 +5108,14 @@ function serializeTypeLiteral(value) {
4904
5108
  if (value === null) return "null";
4905
5109
  return JSON.stringify(value);
4906
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
+ }
4907
5119
 
4908
5120
  //#endregion
4909
5121
  //#region src/lib/plan.ts
@@ -5166,24 +5378,100 @@ function registerProjectsCommands(program) {
5166
5378
  });
5167
5379
  }
5168
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
+
5169
5447
  //#endregion
5170
5448
  //#region src/lib/typegen.ts
5171
- async function runTypegen(runtime, outPath = runtime.typesOut) {
5172
- 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 = {}) {
5173
5453
  const absolute = path.resolve(outPath);
5174
- const source = generateTypesSource(flags);
5175
- fs.mkdirSync(path.dirname(absolute), { recursive: true });
5176
- 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");
5177
5456
  return {
5457
+ source: language === "go" ? generateGoTypesSource(flags, options.packageName ?? runtime.typesPackage ?? "flags") : generateTypesSource(flags),
5178
5458
  written: absolute,
5179
5459
  flagCount: flags.length
5180
5460
  };
5181
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
+ }
5182
5470
 
5183
5471
  //#endregion
5184
5472
  //#region src/commands/pull.ts
5185
5473
  function registerPullCommand(program) {
5186
- 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) => {
5187
5475
  const runtime = getRuntime(command);
5188
5476
  const client = await createCliApiClient(runtime);
5189
5477
  const { projectId, environmentId } = await resolveIds(client, runtime);
@@ -5200,11 +5488,13 @@ function registerPullCommand(program) {
5200
5488
  variants: flag.variants,
5201
5489
  disabled: flag.disabled
5202
5490
  }));
5203
- 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);
5204
5494
  const outPath = path.resolve(runtime.configFile);
5205
5495
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
5206
5496
  fs.writeFileSync(outPath, source);
5207
- const types = await runTypegen(runtime);
5497
+ const types = writeTypegen(preparedTypes);
5208
5498
  printResult(runtime, {
5209
5499
  written: outPath,
5210
5500
  flagCount: localFlags.length,
@@ -5259,6 +5549,7 @@ function registerPushCommand(program) {
5259
5549
  }
5260
5550
  if (planHasDefinitionChanges(plan) && !runtime.json) console.error("Note: definition/ownership changes apply to all environments (requires production permission).");
5261
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);
5262
5553
  const expectedVersions = expectedVersionsFromChanges(planned.changes);
5263
5554
  const applied = await client.flag.push({
5264
5555
  projectId,
@@ -5271,7 +5562,7 @@ function registerPushCommand(program) {
5271
5562
  expectedVersions
5272
5563
  }).catch(mapApiError);
5273
5564
  const appliedPlan = buildSyncPlan(applied.changes);
5274
- const types = await runTypegen(runtime);
5565
+ const types = writeTypegen(preparedTypes);
5275
5566
  const approvalRequests = applied.approvalRequests ?? [];
5276
5567
  printResult(runtime, {
5277
5568
  plan: appliedPlan,
@@ -5366,9 +5657,14 @@ function registerSnapshotsCommands(program) {
5366
5657
  //#endregion
5367
5658
  //#region src/commands/types.ts
5368
5659
  function registerTypesCommands(program) {
5369
- 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) => {
5370
5661
  const runtime = getRuntime(command);
5371
- 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
+ });
5372
5668
  printResult(runtime, result, `Wrote types for ${result.flagCount} flag(s) to ${result.written}`);
5373
5669
  });
5374
5670
  }
@@ -5377,7 +5673,7 @@ function registerTypesCommands(program) {
5377
5673
  //#region src/index.ts
5378
5674
  /** Detected before parse so usage errors can suppress Commander text under `--json`. */
5379
5675
  const jsonRequested = process.argv.includes("--json");
5380
- 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").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);
5381
5677
  if (jsonRequested) program.configureOutput({
5382
5678
  writeErr: () => {},
5383
5679
  writeOut: () => {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipdit/cli",
3
- "version": "0.4.1",
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": {