@lizard-build/cli 0.1.0 → 0.3.29

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 (178) hide show
  1. package/.github/workflows/release.yml +90 -0
  2. package/README.md +41 -0
  3. package/dist/commands/add.js +318 -45
  4. package/dist/commands/add.js.map +1 -1
  5. package/dist/commands/config.d.ts +2 -0
  6. package/dist/commands/config.js +68 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/commands/docs.d.ts +2 -0
  9. package/dist/commands/docs.js +13 -0
  10. package/dist/commands/docs.js.map +1 -0
  11. package/dist/commands/domain.d.ts +9 -0
  12. package/dist/commands/domain.js +195 -0
  13. package/dist/commands/domain.js.map +1 -0
  14. package/dist/commands/git.js +175 -36
  15. package/dist/commands/git.js.map +1 -1
  16. package/dist/commands/init.d.ts +24 -0
  17. package/dist/commands/init.js +128 -86
  18. package/dist/commands/init.js.map +1 -1
  19. package/dist/commands/link.d.ts +7 -0
  20. package/dist/commands/link.js +104 -33
  21. package/dist/commands/link.js.map +1 -1
  22. package/dist/commands/login.js +4 -3
  23. package/dist/commands/login.js.map +1 -1
  24. package/dist/commands/logs.js +223 -30
  25. package/dist/commands/logs.js.map +1 -1
  26. package/dist/commands/open.js +3 -2
  27. package/dist/commands/open.js.map +1 -1
  28. package/dist/commands/port.d.ts +7 -0
  29. package/dist/commands/port.js +49 -0
  30. package/dist/commands/port.js.map +1 -0
  31. package/dist/commands/projects.js +36 -6
  32. package/dist/commands/projects.js.map +1 -1
  33. package/dist/commands/ps.js +32 -39
  34. package/dist/commands/ps.js.map +1 -1
  35. package/dist/commands/redeploy.js +48 -8
  36. package/dist/commands/redeploy.js.map +1 -1
  37. package/dist/commands/regions.js +2 -5
  38. package/dist/commands/regions.js.map +1 -1
  39. package/dist/commands/restart.js +84 -10
  40. package/dist/commands/restart.js.map +1 -1
  41. package/dist/commands/run.d.ts +9 -0
  42. package/dist/commands/run.js +61 -22
  43. package/dist/commands/run.js.map +1 -1
  44. package/dist/commands/scale.d.ts +10 -0
  45. package/dist/commands/scale.js +166 -0
  46. package/dist/commands/scale.js.map +1 -0
  47. package/dist/commands/secrets.js +200 -89
  48. package/dist/commands/secrets.js.map +1 -1
  49. package/dist/commands/service-set.d.ts +49 -0
  50. package/dist/commands/service-set.js +552 -0
  51. package/dist/commands/service-set.js.map +1 -0
  52. package/dist/commands/service-show.d.ts +11 -0
  53. package/dist/commands/service-show.js +44 -0
  54. package/dist/commands/service-show.js.map +1 -0
  55. package/dist/commands/service.d.ts +8 -0
  56. package/dist/commands/service.js +262 -0
  57. package/dist/commands/service.js.map +1 -0
  58. package/dist/commands/ssh.d.ts +2 -0
  59. package/dist/commands/ssh.js +161 -0
  60. package/dist/commands/ssh.js.map +1 -0
  61. package/dist/commands/status.d.ts +7 -0
  62. package/dist/commands/status.js +49 -38
  63. package/dist/commands/status.js.map +1 -1
  64. package/dist/commands/unlink.d.ts +5 -0
  65. package/dist/commands/unlink.js +18 -0
  66. package/dist/commands/unlink.js.map +1 -0
  67. package/dist/commands/up.d.ts +9 -0
  68. package/dist/commands/up.js +417 -0
  69. package/dist/commands/up.js.map +1 -0
  70. package/dist/commands/upgrade.d.ts +2 -0
  71. package/dist/commands/upgrade.js +79 -0
  72. package/dist/commands/upgrade.js.map +1 -0
  73. package/dist/commands/whoami.js +26 -6
  74. package/dist/commands/whoami.js.map +1 -1
  75. package/dist/commands/workspace.d.ts +8 -0
  76. package/dist/commands/workspace.js +36 -0
  77. package/dist/commands/workspace.js.map +1 -0
  78. package/dist/index.js +204 -82
  79. package/dist/index.js.map +1 -1
  80. package/dist/lib/api.d.ts +17 -2
  81. package/dist/lib/api.js +85 -51
  82. package/dist/lib/api.js.map +1 -1
  83. package/dist/lib/auth.d.ts +3 -11
  84. package/dist/lib/auth.js +16 -36
  85. package/dist/lib/auth.js.map +1 -1
  86. package/dist/lib/config.d.ts +36 -15
  87. package/dist/lib/config.js +71 -58
  88. package/dist/lib/config.js.map +1 -1
  89. package/dist/lib/format.d.ts +1 -0
  90. package/dist/lib/format.js +17 -4
  91. package/dist/lib/format.js.map +1 -1
  92. package/dist/lib/name.d.ts +11 -0
  93. package/dist/lib/name.js +26 -0
  94. package/dist/lib/name.js.map +1 -0
  95. package/dist/lib/picker.d.ts +32 -0
  96. package/dist/lib/picker.js +91 -0
  97. package/dist/lib/picker.js.map +1 -0
  98. package/dist/lib/resolve.d.ts +85 -0
  99. package/dist/lib/resolve.js +203 -0
  100. package/dist/lib/resolve.js.map +1 -0
  101. package/dist/lib/updater.d.ts +16 -0
  102. package/dist/lib/updater.js +102 -0
  103. package/dist/lib/updater.js.map +1 -0
  104. package/lizard-wrapper.sh +2 -0
  105. package/package.json +11 -3
  106. package/src/commands/add.ts +388 -56
  107. package/src/commands/config.ts +80 -0
  108. package/src/commands/docs.ts +15 -0
  109. package/src/commands/domain.ts +248 -0
  110. package/src/commands/git.ts +201 -40
  111. package/src/commands/init.ts +149 -100
  112. package/src/commands/link.ts +127 -35
  113. package/src/commands/login.ts +4 -3
  114. package/src/commands/logs.ts +283 -27
  115. package/src/commands/open.ts +3 -2
  116. package/src/commands/port.ts +57 -0
  117. package/src/commands/projects.ts +43 -6
  118. package/src/commands/ps.ts +39 -60
  119. package/src/commands/redeploy.ts +51 -10
  120. package/src/commands/regions.ts +2 -6
  121. package/src/commands/restart.ts +84 -10
  122. package/src/commands/run.ts +68 -24
  123. package/src/commands/scale.ts +216 -0
  124. package/src/commands/secrets.ts +277 -100
  125. package/src/commands/service-set.ts +669 -0
  126. package/src/commands/service-show.ts +52 -0
  127. package/src/commands/service.ts +298 -0
  128. package/src/commands/ssh.ts +176 -0
  129. package/src/commands/status.ts +51 -46
  130. package/src/commands/unlink.ts +17 -0
  131. package/src/commands/up.ts +461 -0
  132. package/src/commands/upgrade.ts +87 -0
  133. package/src/commands/whoami.ts +34 -6
  134. package/src/commands/workspace.ts +44 -0
  135. package/src/index.ts +214 -85
  136. package/src/lib/api.ts +114 -51
  137. package/src/lib/auth.ts +22 -46
  138. package/src/lib/config.ts +100 -65
  139. package/src/lib/format.ts +18 -4
  140. package/src/lib/name.ts +27 -0
  141. package/src/lib/picker.ts +133 -0
  142. package/src/lib/resolve.ts +285 -0
  143. package/src/lib/updater.ts +106 -0
  144. package/test/cli.test.ts +491 -0
  145. package/test/fixtures/hello-app/Dockerfile +5 -0
  146. package/test/fixtures/hello-app/index.js +5 -0
  147. package/test/unit/api.test.ts +66 -0
  148. package/test/unit/config.test.ts +94 -0
  149. package/test/unit/init.test.ts +211 -0
  150. package/test/unit/json.test.ts +208 -0
  151. package/test/unit/picker.test.ts +161 -0
  152. package/test/unit/resolve.test.ts +124 -0
  153. package/test/unit/service-set.test.ts +355 -0
  154. package/vitest.config.ts +10 -0
  155. package/dist/commands/connect.d.ts +0 -2
  156. package/dist/commands/connect.js +0 -117
  157. package/dist/commands/connect.js.map +0 -1
  158. package/dist/commands/context.d.ts +0 -2
  159. package/dist/commands/context.js +0 -71
  160. package/dist/commands/context.js.map +0 -1
  161. package/dist/commands/deploy.d.ts +0 -2
  162. package/dist/commands/deploy.js +0 -120
  163. package/dist/commands/deploy.js.map +0 -1
  164. package/dist/commands/destroy.d.ts +0 -2
  165. package/dist/commands/destroy.js +0 -51
  166. package/dist/commands/destroy.js.map +0 -1
  167. package/dist/commands/update.d.ts +0 -2
  168. package/dist/commands/update.js +0 -41
  169. package/dist/commands/update.js.map +0 -1
  170. package/dist/commands/version.d.ts +0 -2
  171. package/dist/commands/version.js +0 -37
  172. package/dist/commands/version.js.map +0 -1
  173. package/src/commands/connect.ts +0 -145
  174. package/src/commands/context.ts +0 -93
  175. package/src/commands/deploy.ts +0 -153
  176. package/src/commands/destroy.ts +0 -51
  177. package/src/commands/update.ts +0 -44
  178. package/src/commands/version.ts +0 -37
package/src/index.ts CHANGED
@@ -1,126 +1,247 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from "commander";
4
+ import chalk from "chalk";
4
5
  import { setJSONMode, isJSONMode, error } from "./lib/format.js";
5
- import { setTokenOverride, requireAuth, isLoggedIn } from "./lib/auth.js";
6
- import { setBaseURL, setAccessToken } from "./lib/api.js";
6
+ import { requireAuth, isLoggedIn } from "./lib/auth.js";
7
+ import { setBaseURL, setAccessToken, APIError } from "./lib/api.js";
8
+ import { checkForUpdateInBackground, CURRENT_VERSION } from "./lib/updater.js";
7
9
 
8
- // Commands
9
- import { registerLogin } from "./commands/login.js";
10
- import { registerLogout } from "./commands/logout.js";
11
- import { registerWhoami } from "./commands/whoami.js";
10
+ const BANNER = chalk.rgb(16, 185, 129)(
11
+ [
12
+ "╔═══════════════════════════════════════════════════╗",
13
+ "║ ║",
14
+ "║ ██╗ ██╗███████╗ █████╗ ██████╗ ██████╗ ║",
15
+ "║ ██║ ██║╚══███╔╝██╔══██╗██╔══██╗██╔══██╗ ║",
16
+ "║ ██║ ██║ ███╔╝ ███████║██████╔╝██║ ██║ ║",
17
+ "║ ██║ ██║ ███╔╝ ██╔══██║██╔══██╗██║ ██║ ║",
18
+ "║ ███████╗██║███████╗██║ ██║██║ ██║██████╔╝ ║",
19
+ "║ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ║",
20
+ "║ ║",
21
+ "╚═══════════════════════════════════════════════════╝",
22
+ ].join("\n"),
23
+ );
24
+
25
+ // Commands (alphabetical by command name)
26
+ import { registerAdd } from "./commands/add.js";
27
+ import { registerConfig } from "./commands/config.js";
28
+ import { registerDocs } from "./commands/docs.js";
29
+ import { registerDomain } from "./commands/domain.js";
30
+ import { registerGit } from "./commands/git.js";
12
31
  import { registerInit } from "./commands/init.js";
13
32
  import { registerLink } from "./commands/link.js";
33
+ import { registerLogin } from "./commands/login.js";
34
+ import { registerLogout } from "./commands/logout.js";
35
+ import { registerLogs } from "./commands/logs.js";
36
+ import { registerOpen } from "./commands/open.js";
37
+ import { registerPort } from "./commands/port.js";
14
38
  import { registerProjects } from "./commands/projects.js";
15
- import { registerDeploy } from "./commands/deploy.js";
16
39
  import { registerPs } from "./commands/ps.js";
17
- import { registerAdd } from "./commands/add.js";
18
- import { registerDestroy } from "./commands/destroy.js";
19
- import { registerRestart } from "./commands/restart.js";
20
40
  import { registerRedeploy } from "./commands/redeploy.js";
21
- import { registerLogs } from "./commands/logs.js";
22
- import { registerSecrets } from "./commands/secrets.js";
23
41
  import { registerRegions } from "./commands/regions.js";
24
- import { registerStatus } from "./commands/status.js";
25
- import { registerOpen } from "./commands/open.js";
42
+ import { registerRestart } from "./commands/restart.js";
26
43
  import { registerRun } from "./commands/run.js";
27
- import { registerConnect } from "./commands/connect.js";
28
- import { registerContext } from "./commands/context.js";
29
- import { registerGit } from "./commands/git.js";
30
- import { registerVersion } from "./commands/version.js";
31
- import { registerUpdate } from "./commands/update.js";
44
+ import { registerScale } from "./commands/scale.js";
45
+ import { registerSecrets } from "./commands/secrets.js";
46
+ import { registerService } from "./commands/service.js";
47
+ import { registerSSH } from "./commands/ssh.js";
48
+ import { registerStatus } from "./commands/status.js";
49
+ import { registerUnlink } from "./commands/unlink.js";
50
+ import { registerUp } from "./commands/up.js";
51
+ import { registerUpgrade } from "./commands/upgrade.js";
52
+ import { registerWhoami } from "./commands/whoami.js";
53
+ import { registerWorkspace } from "./commands/workspace.js";
32
54
 
33
55
  const program = new Command();
34
56
 
35
57
  program
36
58
  .name("lizard")
37
59
  .description("Lizard CLI — deploy and manage apps on Lizard")
38
- .version("0.1.0")
39
- .option("--json", "Output in JSON format")
40
- .option("-y, --yes", "Skip confirmation prompts")
41
- .option("--workspace <id>", "Workspace name or ID")
42
- .option("--project <id>", "Project ID")
43
- .option("--environment <name>", "Environment name")
44
- .option("--region <region>", "Region for creating services")
45
- .option("--token <token>", "API token")
46
- .option("--no-color", "Disable colors")
47
- .option("--verbose", "Verbose output")
60
+ .version(CURRENT_VERSION)
61
+ .addHelpText("before", BANNER + "\n")
62
+ .configureHelp({
63
+ subcommandTerm: (cmd) => {
64
+ const alias = cmd.aliases()[0];
65
+ return alias ? `${cmd.name()}|${alias}` : cmd.name();
66
+ },
67
+ })
68
+ .option("--json", "Output in JSON format (combine with --help to dump machine-readable command schema for agents)")
48
69
  .hook("preAction", async (thisCommand, actionCommand) => {
49
70
  const opts = thisCommand.opts();
50
71
 
72
+ // Check for updates silently in background (shows notice after command)
73
+ if (actionCommand.name() !== "upgrade") {
74
+ checkForUpdateInBackground();
75
+ }
76
+
51
77
  // JSON mode: explicit flag or non-TTY stdout
52
78
  if (opts.json || !process.stdout.isTTY) {
53
79
  setJSONMode(true);
54
80
  }
55
81
 
56
- // Token override
57
- if (opts.token) {
58
- setTokenOverride(opts.token);
59
- }
60
-
61
82
  // API URL override
62
83
  if (process.env.LIZARD_API_URL) {
63
84
  setBaseURL(process.env.LIZARD_API_URL);
64
85
  }
65
86
 
66
- // Commands that don't need auth
67
- const noAuth = new Set(["login", "logout", "version", "completion", "update", "help"]);
68
- if (noAuth.has(actionCommand.name())) return;
87
+ // Top-level commands that don't need auth. `status` prints the local link;
88
+ // the optional workspace backfill silently no-ops when not authed.
89
+ //
90
+ // Match by name AND parent — otherwise leaf subcommands sharing a name
91
+ // (e.g. `git status`, `up status`) skip the auto-login flow and would
92
+ // 401 instead of prompting the user to log in.
93
+ const noAuth = new Set(["login", "logout", "upgrade", "help", "docs", "status"]);
94
+ const isTopLevel = actionCommand.parent === thisCommand;
95
+ if (isTopLevel && noAuth.has(actionCommand.name())) return;
69
96
 
70
97
  // Require auth — auto-triggers login flow if not logged in
71
98
  const creds = await requireAuth();
72
99
  setAccessToken(creds.accessToken);
73
100
  });
74
101
 
75
- // Register all commands
76
- registerLogin(program);
77
- registerLogout(program);
78
- registerWhoami(program);
102
+ // Register all commands (alphabetical)
103
+ registerAdd(program);
104
+ registerConfig(program);
105
+ registerDocs(program);
106
+ registerDomain(program);
107
+ registerGit(program);
79
108
  registerInit(program);
80
109
  registerLink(program);
110
+ registerLogin(program);
111
+ registerLogout(program);
112
+ registerLogs(program);
113
+ registerOpen(program);
114
+ registerPort(program);
81
115
  registerProjects(program);
82
- registerDeploy(program);
83
116
  registerPs(program);
84
- registerAdd(program);
85
- registerDestroy(program);
86
- registerRestart(program);
87
117
  registerRedeploy(program);
88
- registerLogs(program);
89
- registerSecrets(program);
90
118
  registerRegions(program);
91
- registerStatus(program);
92
- registerOpen(program);
119
+ registerRestart(program);
93
120
  registerRun(program);
94
- registerConnect(program);
95
- registerContext(program);
96
- registerGit(program);
97
- registerVersion(program);
98
- registerUpdate(program);
99
-
100
- // Shell completion
101
- program
102
- .command("completion")
103
- .argument("<shell>", "Shell type (bash, zsh, fish)")
104
- .description("Generate shell completion script")
105
- .action((shell: string) => {
106
- // Commander doesn't have built-in completion like Cobra
107
- // Point users to manual setup
108
- console.log(`# Add to your .${shell}rc:`);
109
- if (shell === "bash") {
110
- console.log(`eval "$(lizard completion bash)"`);
111
- } else if (shell === "zsh") {
112
- console.log(`# lizard completion is not yet available for zsh`);
113
- console.log(`# Coming soon`);
114
- } else if (shell === "fish") {
115
- console.log(`# lizard completion is not yet available for fish`);
116
- console.log(`# Coming soon`);
117
- }
118
- });
121
+ registerScale(program);
122
+ registerSecrets(program);
123
+ registerService(program);
124
+ registerSSH(program);
125
+ registerStatus(program);
126
+ registerUnlink(program);
127
+ registerUp(program);
128
+ registerUpgrade(program);
129
+ registerWhoami(program);
130
+ registerWorkspace(program);
119
131
 
120
132
  // Error handling
121
133
  program.exitOverride();
122
134
 
135
+ const EXIT_CODES: Record<string, string> = {
136
+ "0": "success",
137
+ "1": "generic error",
138
+ "2": "auth (401/403)",
139
+ "3": "not found (404)",
140
+ "4": "timeout (408/504)",
141
+ "5": "cancelled by user",
142
+ };
143
+
144
+ function isHelpJsonRequest(argv: string[]): boolean {
145
+ const hasHelp = argv.some((a) => a === "--help" || a === "-h");
146
+ const hasJson = argv.some((a) => a === "--json");
147
+ return hasHelp && hasJson;
148
+ }
149
+
150
+ function collectValueFlags(cmd: Command, acc: Set<string>) {
151
+ for (const opt of cmd.options as any[]) {
152
+ if (opt.required || opt.optional) {
153
+ if (opt.short) acc.add(opt.short);
154
+ if (opt.long) acc.add(opt.long);
155
+ }
156
+ }
157
+ for (const sub of cmd.commands) collectValueFlags(sub, acc);
158
+ }
159
+
160
+ function findTargetCommand(argv: string[], root: Command): Command {
161
+ const valueFlags = new Set<string>();
162
+ collectValueFlags(root, valueFlags);
163
+
164
+ let cur: Command = root;
165
+ for (let i = 2; i < argv.length; i++) {
166
+ const tok = argv[i];
167
+ if (tok === "--help" || tok === "-h" || tok === "--json") continue;
168
+ if (tok.startsWith("-")) {
169
+ if (tok.includes("=")) continue;
170
+ if (valueFlags.has(tok) && i + 1 < argv.length) i++;
171
+ continue;
172
+ }
173
+ const sub = cur.commands.find(
174
+ (c) => c.name() === tok || c.aliases().includes(tok),
175
+ );
176
+ if (!sub) break;
177
+ cur = sub;
178
+ }
179
+ return cur;
180
+ }
181
+
182
+ function dumpOption(o: any) {
183
+ return {
184
+ flags: o.flags,
185
+ long: o.long ?? null,
186
+ short: o.short ?? null,
187
+ description: o.description ?? "",
188
+ takesValue: Boolean(o.required || o.optional),
189
+ valueRequired: Boolean(o.required),
190
+ defaultValue: o.defaultValue ?? null,
191
+ choices: o.argChoices ?? null,
192
+ negate: Boolean(o.negate),
193
+ };
194
+ }
195
+
196
+ function dumpCommand(cmd: Command): any {
197
+ const args = ((cmd as any).registeredArguments ?? []).map((a: any) => ({
198
+ name: a.name(),
199
+ description: a.description ?? "",
200
+ required: Boolean(a.required),
201
+ variadic: Boolean(a.variadic),
202
+ defaultValue: a.defaultValue ?? null,
203
+ choices: a.argChoices ?? null,
204
+ }));
205
+ return {
206
+ name: cmd.name(),
207
+ aliases: cmd.aliases(),
208
+ description: cmd.description(),
209
+ usage: cmd.usage(),
210
+ arguments: args,
211
+ options: (cmd.options as any[])
212
+ .filter((o) => !o.hidden)
213
+ .map(dumpOption),
214
+ subcommands: cmd.commands
215
+ .filter((c: any) => !c._hidden)
216
+ .map(dumpCommand),
217
+ };
218
+ }
219
+
123
220
  async function main() {
221
+ // Set JSON mode from argv *before* parseAsync so the catch block below
222
+ // honors --json even when commander rejects before our preAction hook
223
+ // fires (e.g. unknown command, malformed global flag). Non-TTY auto-mode
224
+ // stays in preAction — `lizard --help | less` shouldn't suddenly emit JSON.
225
+ if (process.argv.includes("--json")) {
226
+ setJSONMode(true);
227
+ }
228
+
229
+ if (isHelpJsonRequest(process.argv)) {
230
+ const target = findTargetCommand(process.argv, program);
231
+ const isRoot = target === program;
232
+ const out = {
233
+ cli: "lizard",
234
+ version: CURRENT_VERSION,
235
+ command: dumpCommand(target),
236
+ globalOptions: isRoot
237
+ ? []
238
+ : (program.options as any[]).filter((o) => !o.hidden).map(dumpOption),
239
+ exitCodes: EXIT_CODES,
240
+ };
241
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
242
+ process.exit(0);
243
+ }
244
+
124
245
  try {
125
246
  await program.parseAsync(process.argv);
126
247
  } catch (err: any) {
@@ -133,14 +254,19 @@ async function main() {
133
254
  }
134
255
 
135
256
  const msg = err.message || String(err);
257
+ const apiErr = err instanceof APIError ? err : undefined;
258
+ const status = apiErr?.status;
259
+ const code = apiErr?.code || err.code || "ERROR";
136
260
 
137
261
  if (isJSONMode()) {
138
262
  console.log(
139
263
  JSON.stringify(
140
264
  {
141
265
  error: {
142
- code: err.code || "ERROR",
266
+ code,
267
+ status: status ?? null,
143
268
  message: msg,
269
+ body: apiErr?.body ?? null,
144
270
  },
145
271
  },
146
272
  null,
@@ -151,16 +277,19 @@ async function main() {
151
277
  error(msg);
152
278
  }
153
279
 
154
- // Exit codes per spec
155
- if (msg.includes("Not authenticated") || msg.includes("Invalid token")) {
156
- process.exit(2);
157
- }
158
- if (msg.includes("not found") || msg.includes("Not found")) {
159
- process.exit(3);
160
- }
161
- if (msg.includes("timeout") || msg.includes("Timeout")) {
162
- process.exit(4);
163
- }
280
+ // Exit codes derived from APIError.status (or tagged error codes), not message text
281
+ const isAuth = status === 401 || status === 403 || code === "NOT_AUTHENTICATED";
282
+ const isNotFound = status === 404;
283
+ const isTimeout =
284
+ status === 408 ||
285
+ status === 504 ||
286
+ err.name === "AbortError" ||
287
+ err.code === "ETIMEDOUT" ||
288
+ err.code === "UND_ERR_CONNECT_TIMEOUT";
289
+
290
+ if (isAuth) process.exit(2);
291
+ if (isNotFound) process.exit(3);
292
+ if (isTimeout) process.exit(4);
164
293
  process.exit(1);
165
294
  }
166
295
  }
package/src/lib/api.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { getToken } from "./auth.js";
2
+ import { CURRENT_VERSION } from "./updater.js";
3
+ import * as https from "node:https";
4
+ import * as http from "node:http";
2
5
 
3
6
  const DEFAULT_BASE_URL = "https://lizard.build";
4
- const USER_AGENT = "lizard-cli/0.1";
7
+ const USER_AGENT = `lizard-cli/${CURRENT_VERSION}`;
5
8
 
6
9
  let baseURL = process.env.LIZARD_API_URL || DEFAULT_BASE_URL;
7
10
  let _accessToken: string | null = null;
@@ -10,13 +13,56 @@ export function setBaseURL(url: string) { baseURL = url; }
10
13
  export function getBaseURL() { return baseURL; }
11
14
  export function setAccessToken(token: string) { _accessToken = token; }
12
15
 
16
+ // ── Scoping ───────────────────────────────────────────────────────────
17
+ //
18
+ // Every project-scoped endpoint takes `?workspaceId=…` — mirrors
19
+ // lizard-client's `withScope` so server-side state is shared across
20
+ // CLI and browser. Build URLs through these helpers, never by hand.
21
+
22
+ export interface ResourceScope {
23
+ workspaceId?: string | null;
24
+ }
25
+
26
+ export interface Workspace {
27
+ id: string;
28
+ name: string;
29
+ slug: string;
30
+ role: "owner" | "member";
31
+ isPersonal?: boolean;
32
+ projectCount?: number;
33
+ createdAt?: number;
34
+ }
35
+
36
+ export function withQuery(
37
+ path: string,
38
+ params: Record<string, string | number | boolean | null | undefined>,
39
+ ): string {
40
+ const search = new URLSearchParams();
41
+ for (const [key, value] of Object.entries(params)) {
42
+ if (value === null || value === undefined || value === "") continue;
43
+ search.set(key, String(value));
44
+ }
45
+ const query = search.toString();
46
+ if (!query) return path;
47
+ return `${path}${path.includes("?") ? "&" : "?"}${query}`;
48
+ }
49
+
50
+ export function withScope(path: string, scope?: ResourceScope): string {
51
+ if (!scope) return path;
52
+ return withQuery(path, {
53
+ workspaceId: scope.workspaceId,
54
+ });
55
+ }
56
+
13
57
  export class APIError extends Error {
14
58
  status: number;
15
59
  code: string;
16
- constructor(status: number, message: string, code = "") {
60
+ body: unknown;
61
+ constructor(status: number, message: string, code = "", body: unknown = null) {
17
62
  super(message);
18
63
  this.status = status;
19
64
  this.code = code;
65
+ this.body = body;
20
66
  }
21
67
  }
22
68
 
@@ -32,12 +78,14 @@ async function request<T = any>(
32
78
  method: string,
33
79
  path: string,
34
80
  body?: unknown,
81
+ extraHeaders: Record<string, string> = {},
35
82
  ): Promise<T> {
36
83
  const url = baseURL + path;
37
84
  const token = _accessToken || getToken();
38
85
 
39
86
  const headers: Record<string, string> = {
40
87
  "User-Agent": USER_AGENT,
88
+ ...extraHeaders,
41
89
  };
42
90
  if (token) {
43
91
  headers["Authorization"] = `Bearer ${token}`;
@@ -55,12 +103,14 @@ async function request<T = any>(
55
103
  if (!res.ok) {
56
104
  let msg = res.statusText;
57
105
  let code = "";
106
+ let body: unknown = null;
58
107
  try {
59
108
  const j = (await res.json()) as any;
109
+ body = j;
60
110
  msg = j.error || j.message || msg;
61
111
  code = j.code || "";
62
112
  } catch {}
63
- throw new APIError(res.status, msg, code);
113
+ throw new APIError(res.status, msg, code, body);
64
114
  }
65
115
 
66
116
  const text = await res.text();
@@ -70,8 +120,8 @@ async function request<T = any>(
70
120
 
71
121
  export const api = {
72
122
  get: <T = any>(path: string) => request<T>("GET", path),
73
- post: <T = any>(path: string, body?: unknown) =>
74
- request<T>("POST", path, body),
123
+ post: <T = any>(path: string, body?: unknown, headers?: Record<string, string>) =>
124
+ request<T>("POST", path, body, headers),
75
125
  put: <T = any>(path: string, body?: unknown) =>
76
126
  request<T>("PUT", path, body),
77
127
  patch: <T = any>(path: string, body?: unknown) =>
@@ -80,55 +130,68 @@ export const api = {
80
130
  };
81
131
 
82
132
  /** Stream SSE and call handler for each data line. Return false to stop. */
83
- export async function streamSSE(
133
+ export function streamSSE(
84
134
  path: string,
85
135
  handler: (event: string, data: string) => boolean | void,
86
136
  ): Promise<void> {
87
- const url = baseURL + path;
88
- const token = _accessToken || getToken();
89
- const headers: Record<string, string> = {
90
- "User-Agent": USER_AGENT,
91
- Accept: "text/event-stream",
92
- };
93
- if (token) headers["Authorization"] = `Bearer ${token}`;
137
+ return new Promise((resolve, reject) => {
138
+ const url = new URL(baseURL + path);
139
+ const token = _accessToken || getToken();
140
+ const reqHeaders: Record<string, string> = {
141
+ "User-Agent": USER_AGENT,
142
+ Accept: "text/event-stream",
143
+ };
144
+ if (token) reqHeaders["Authorization"] = `Bearer ${token}`;
145
+
146
+ const transport = url.protocol === "https:" ? https : http;
147
+ const req = transport.request(
148
+ { hostname: url.hostname, port: url.port || (url.protocol === "https:" ? 443 : 80),
149
+ path: url.pathname + url.search, method: "GET", headers: reqHeaders },
150
+ (res) => {
151
+ if (res.statusCode && res.statusCode >= 400) {
152
+ let body = "";
153
+ res.on("data", (c: Buffer) => body += c.toString());
154
+ res.on("end", () => reject(new APIError(res.statusCode!, `SSE failed: ${body}`)));
155
+ return;
156
+ }
94
157
 
95
- const res = await fetch(url, { headers });
96
- if (!res.ok) {
97
- throw new APIError(res.status, `SSE failed: ${res.statusText}`);
98
- }
99
- if (!res.body) return;
100
-
101
- const reader = res.body.getReader();
102
- const decoder = new TextDecoder();
103
- let buffer = "";
104
- let currentEvent = "";
105
- let currentData = "";
106
-
107
- while (true) {
108
- const { done, value } = await reader.read();
109
- if (done) break;
110
-
111
- buffer += decoder.decode(value, { stream: true });
112
- const lines = buffer.split("\n");
113
- buffer = lines.pop() || "";
114
-
115
- for (const line of lines) {
116
- const trimmed = line.replace(/\r$/, "");
117
- if (trimmed === "") {
118
- if (currentData) {
119
- const cont = handler(currentEvent, currentData);
120
- if (cont === false) {
121
- reader.cancel();
122
- return;
158
+ let buffer = "";
159
+ let currentEvent = "";
160
+ let currentData = "";
161
+
162
+ res.setEncoding("utf8");
163
+ res.on("data", (chunk: string) => {
164
+ buffer += chunk;
165
+ const lines = buffer.split("\n");
166
+ buffer = lines.pop() ?? "";
167
+
168
+ for (const line of lines) {
169
+ const trimmed = line.replace(/\r$/, "");
170
+ if (trimmed === "") {
171
+ if (currentData) {
172
+ const cont = handler(currentEvent, currentData);
173
+ if (cont === false) {
174
+ req.destroy();
175
+ resolve();
176
+ return;
177
+ }
178
+ }
179
+ currentEvent = "";
180
+ currentData = "";
181
+ } else if (trimmed.startsWith("event:")) {
182
+ currentEvent = trimmed.slice(6).trim();
183
+ } else if (trimmed.startsWith("data:")) {
184
+ currentData = trimmed.slice(5).trimStart();
185
+ }
123
186
  }
124
- }
125
- currentEvent = "";
126
- currentData = "";
127
- } else if (trimmed.startsWith("event:")) {
128
- currentEvent = trimmed.slice(6).trim();
129
- } else if (trimmed.startsWith("data:")) {
130
- currentData = trimmed.slice(5).trimStart();
131
- }
132
- }
133
- }
187
+ });
188
+
189
+ res.on("end", resolve);
190
+ res.on("error", reject);
191
+ },
192
+ );
193
+
194
+ req.on("error", reject);
195
+ req.end();
196
+ });
134
197
  }