@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
@@ -1,28 +1,132 @@
1
1
  import chalk from "chalk";
2
- import { Command } from "commander";
3
- import { api } from "../lib/api.js";
4
- import { resolveProjectId } from "../lib/config.js";
5
- import { success, isJSONMode, printJSON, table } from "../lib/format.js";
2
+ import * as p from "@clack/prompts";
3
+ import { Command, Option } from "commander";
4
+ import { api, withScope, type ResourceScope } from "../lib/api.js";
5
+ import { getProjectLink } from "../lib/config.js";
6
+ import { getActiveService, resolveProjectScope } from "../lib/resolve.js";
7
+ import { success, isJSONMode, printJSON, table, isTTY } from "../lib/format.js";
6
8
 
7
9
  interface Secret {
8
10
  key: string;
9
11
  value: string;
10
12
  }
11
13
 
12
- export function registerSecrets(program: Command) {
13
- const secret = program
14
- .command("secret")
15
- .description("Manage project secrets");
14
+ /**
15
+ * `lizard secrets` — secret management. Defaults to the linked service
16
+ * scope, with --global for project-wide.
17
+ *
18
+ * Bare command without subcommand prints the secret list.
19
+ * `--refs` lists `${{...}}` templates available in this scope.
20
+ */
21
+ interface Scope {
22
+ path: string;
23
+ label: "project" | "service";
24
+ projectId: string;
25
+ serviceId?: string;
26
+ serviceName?: string;
27
+ scope: ResourceScope;
28
+ }
16
29
 
17
- secret
18
- .command("list")
19
- .description("List project secrets")
20
- .option("--show", "Reveal secret values")
21
- .action(async (opts) => {
22
- const projectId = resolveProjectId(program.opts().project);
23
- const secrets = await api.get<Secret[]>(
24
- `/api/projects/${projectId}/secrets`,
30
+ async function resolveScope(
31
+ projectFlag: string | undefined,
32
+ serviceFlag: string | undefined,
33
+ global: boolean,
34
+ ): Promise<Scope> {
35
+ const { projectId, scope: rs } = await resolveProjectScope(projectFlag);
36
+
37
+ if (global) {
38
+ return {
39
+ path: withScope(`/api/projects/${projectId}/secrets`, rs),
40
+ label: "project",
41
+ projectId,
42
+ scope: rs,
43
+ };
44
+ }
45
+
46
+ if (serviceFlag) {
47
+ const svc = await getActiveService(serviceFlag, projectId);
48
+ return {
49
+ path: withScope(`/api/apps/${svc.id}/secrets`, rs),
50
+ label: "service",
51
+ projectId,
52
+ serviceId: svc.id,
53
+ serviceName: svc.name,
54
+ scope: rs,
55
+ };
56
+ }
57
+
58
+ const link = getProjectLink();
59
+ if (!link?.serviceId) {
60
+ throw new Error(
61
+ "No service linked. Pass --service <name>, run `lizard service link <name>`, or use --global.",
62
+ );
63
+ }
64
+ return {
65
+ path: withScope(`/api/apps/${link.serviceId}/secrets`, rs),
66
+ label: "service",
67
+ projectId,
68
+ serviceId: link.serviceId,
69
+ serviceName: link.serviceName || link.serviceId,
70
+ scope: rs,
71
+ };
72
+ }
73
+
74
+ async function configApplySecrets(
75
+ scope: Scope,
76
+ secrets: Record<string, string | null>,
77
+ ): Promise<void> {
78
+ const payload =
79
+ scope.label === "project"
80
+ ? { secrets: { shared: secrets } }
81
+ : { secrets: { services: { [scope.serviceName!]: secrets } } };
82
+ await api.post(
83
+ withScope(`/api/projects/${scope.projectId}/config:apply`, scope.scope),
84
+ payload,
85
+ );
86
+ }
87
+
88
+ function parsePairs(pairs: string[]): Record<string, string> {
89
+ const out: Record<string, string> = {};
90
+ for (const pair of pairs) {
91
+ const eq = pair.indexOf("=");
92
+ if (eq < 1) {
93
+ throw new Error(
94
+ `Invalid format: "${pair}". Use KEY=value (e.g. \`lizard secrets set DATABASE_URL=postgres://...\`).`,
25
95
  );
96
+ }
97
+ out[pair.slice(0, eq)] = pair.slice(eq + 1);
98
+ }
99
+ return out;
100
+ }
101
+
102
+ export function registerSecrets(program: Command) {
103
+ const cmd = program
104
+ .command("secrets")
105
+ .alias("secret")
106
+ .description("Manage secrets (default scope: service; --global for project)")
107
+ .option("--global", "Target the whole project")
108
+ .option("--show", "Reveal values")
109
+ .option("--ref", "List reference templates available in this scope")
110
+ .addOption(new Option("--refs").hideHelp().implies({ ref: true }))
111
+ .option("-s, --service <name>", "Service to scope to (overrides linked)")
112
+ .option("-p, --project <id>", "Project to scope to")
113
+ .addHelpText(
114
+ "after",
115
+ `
116
+ Notes:
117
+ Secrets apply live to running VMs. Keys prefixed with NEXT_PUBLIC_* or VITE_*
118
+ trigger a redeploy because they're baked into the client bundle at build time.`,
119
+ )
120
+ .action(async (opts) => {
121
+ const scope = await resolveScope(opts.project, opts.service, opts.global);
122
+
123
+ // --ref → list reference templates exposed by the platform
124
+ if (opts.ref) {
125
+ await printRefs(scope);
126
+ return;
127
+ }
128
+
129
+ const secrets = await api.get<Secret[]>(scope.path);
26
130
 
27
131
  if (isJSONMode()) {
28
132
  printJSON(
@@ -34,7 +138,9 @@ export function registerSecrets(program: Command) {
34
138
  }
35
139
 
36
140
  if (secrets.length === 0) {
37
- console.log("No secrets. Use `lizard secret set KEY=value`.");
141
+ console.log(
142
+ `No ${scope.label} secrets. Use \`lizard secrets set KEY=value${opts.global ? " --global" : ""}\`.`,
143
+ );
38
144
  return;
39
145
  }
40
146
 
@@ -42,134 +148,205 @@ export function registerSecrets(program: Command) {
42
148
  ["Key", "Value"],
43
149
  secrets.map((s) => [
44
150
  s.key,
45
- opts.show ? s.value : chalk.dim("•".repeat(Math.min(s.value.length, 20))),
151
+ opts.show
152
+ ? s.value
153
+ : chalk.dim("•".repeat(Math.min(s.value.length, 20))),
46
154
  ]),
47
155
  );
48
156
  });
49
157
 
50
- secret
51
- .command("set")
52
- .argument("<pairs...>", "KEY=value pairs")
53
- .description("Set one or more secrets")
54
- .option("--no-redeploy", "Don't trigger redeploy")
55
- .action(async (pairs: string[], opts) => {
56
- const projectId = resolveProjectId(program.opts().project);
158
+ cmd
159
+ .command("list")
160
+ .description("List secrets")
161
+ .option("--global", "Target the whole project")
162
+ .option("--show", "Reveal values")
163
+ .option("--ref", "List reference templates available in this scope")
164
+ .addOption(new Option("--refs").hideHelp().implies({ ref: true }))
165
+ .option("-s, --service <name>", "Service to scope to (overrides linked)")
166
+ .option("-p, --project <id>", "Project to scope to")
167
+ .action(async (opts, sub) => {
168
+ const inherited = sub.parent?.opts() || {};
169
+ const scope = await resolveScope(
170
+ opts.project ?? inherited.project,
171
+ opts.service ?? inherited.service,
172
+ opts.global || inherited.global,
173
+ );
57
174
 
58
- // Parse KEY=value pairs
59
- const newSecrets: Record<string, string> = {};
60
- for (const pair of pairs) {
61
- const eqIdx = pair.indexOf("=");
62
- if (eqIdx < 1) {
63
- throw new Error(`Invalid format: "${pair}". Use KEY=value`);
64
- }
65
- newSecrets[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
175
+ if (opts.ref) {
176
+ await printRefs(scope);
177
+ return;
66
178
  }
67
179
 
68
- // Get existing secrets, merge, update
69
- const existing = await api.get<Secret[]>(
70
- `/api/projects/${projectId}/secrets`,
71
- );
72
- const merged: Secret[] = [];
73
- const existingKeys = new Set<string>();
74
-
75
- for (const s of existing) {
76
- if (newSecrets[s.key] !== undefined) {
77
- merged.push({ key: s.key, value: newSecrets[s.key] });
78
- existingKeys.add(s.key);
79
- } else {
80
- merged.push(s);
81
- }
180
+ const secrets = await api.get<Secret[]>(scope.path);
181
+
182
+ if (isJSONMode()) {
183
+ printJSON(
184
+ opts.show
185
+ ? secrets
186
+ : secrets.map((s) => ({ key: s.key, value: "***" })),
187
+ );
188
+ return;
82
189
  }
83
- // Add new keys
84
- for (const [key, value] of Object.entries(newSecrets)) {
85
- if (!existingKeys.has(key)) {
86
- merged.push({ key, value });
87
- }
190
+
191
+ if (secrets.length === 0) {
192
+ console.log(`No ${scope.label} secrets.`);
193
+ return;
88
194
  }
89
195
 
90
- await api.put(`/api/projects/${projectId}/secrets`, merged);
196
+ table(
197
+ ["Key", "Value"],
198
+ secrets.map((s) => [
199
+ s.key,
200
+ opts.show
201
+ ? s.value
202
+ : chalk.dim("•".repeat(Math.min(s.value.length, 20))),
203
+ ]),
204
+ );
205
+ });
206
+
207
+ cmd
208
+ .command("set")
209
+ .argument("<pairs...>", "KEY=value pairs")
210
+ .description("Set one or more secrets")
211
+ .option("--global", "Target the whole project")
212
+ .option("-s, --service <name>", "Service to scope to (overrides linked)")
213
+ .option("-p, --project <id>", "Project to scope to")
214
+ .action(async (pairs: string[], opts, sub) => {
215
+ const inherited = sub.parent?.opts() || {};
216
+ const scope = await resolveScope(
217
+ opts.project ?? inherited.project,
218
+ opts.service ?? inherited.service,
219
+ opts.global || inherited.global,
220
+ );
221
+ const newSecrets = parsePairs(pairs);
222
+ await configApplySecrets(scope, newSecrets);
91
223
 
92
224
  if (isJSONMode()) {
93
- printJSON({ updated: Object.keys(newSecrets) });
225
+ printJSON({ updated: Object.keys(newSecrets), scope: scope.label });
94
226
  } else {
95
- success(
96
- `${Object.keys(newSecrets).length} secret(s) updated`,
97
- );
227
+ success(`${Object.keys(newSecrets).length} ${scope.label} secret(s) updated`);
98
228
  }
99
229
  });
100
230
 
101
- secret
231
+ cmd
102
232
  .command("delete")
233
+ .alias("rm")
103
234
  .argument("<keys...>", "Secret keys to delete")
104
235
  .description("Delete one or more secrets")
105
- .action(async (keys: string[]) => {
106
- const projectId = resolveProjectId(program.opts().project);
107
- const existing = await api.get<Secret[]>(
108
- `/api/projects/${projectId}/secrets`,
236
+ .option("--global", "Target the whole project")
237
+ .option("-y, --yes", "Skip confirmation")
238
+ .option("-s, --service <name>", "Service to scope to (overrides linked)")
239
+ .option("-p, --project <id>", "Project to scope to")
240
+ .action(async (keys: string[], opts, sub) => {
241
+ const inherited = sub.parent?.opts() || {};
242
+ const scope = await resolveScope(
243
+ opts.project ?? inherited.project,
244
+ opts.service ?? inherited.service,
245
+ opts.global || inherited.global,
109
246
  );
247
+ const existing = await api.get<Secret[]>(scope.path);
248
+ const existingKeys = new Set(existing.map((s) => s.key));
249
+ const notFound = keys.filter((k) => !existingKeys.has(k));
250
+ if (notFound.length > 0) {
251
+ throw new Error(`Secret(s) not found: ${notFound.join(", ")}`);
252
+ }
110
253
 
111
- const keysSet = new Set(keys);
112
- const filtered = existing.filter((s) => !keysSet.has(s.key));
113
-
114
- if (filtered.length === existing.length) {
115
- throw new Error(`Secret(s) not found: ${keys.join(", ")}`);
254
+ if (!opts.yes) {
255
+ if (!isTTY()) throw new Error("Use -y to confirm in non-interactive mode");
256
+ const summary = keys.length === 1 ? chalk.bold(keys[0]) : `${keys.length} keys`;
257
+ const confirm = await p.confirm({
258
+ message: `Delete ${summary} from ${chalk.bold(scope.label)} scope?`,
259
+ });
260
+ if (p.isCancel(confirm) || !confirm) process.exit(5);
116
261
  }
117
262
 
118
- await api.put(`/api/projects/${projectId}/secrets`, filtered);
263
+ const deletePayload: Record<string, null> = {};
264
+ keys.forEach((k) => { deletePayload[k] = null; });
265
+ await configApplySecrets(scope, deletePayload);
119
266
 
120
267
  if (isJSONMode()) {
121
- printJSON({ deleted: keys });
268
+ printJSON({ deleted: keys, scope: scope.label });
122
269
  } else {
123
- success(`${keys.length} secret(s) deleted`);
270
+ success(`${keys.length} ${scope.label} secret(s) deleted`);
124
271
  }
125
272
  });
126
273
 
127
- secret
274
+ cmd
128
275
  .command("import")
129
- .description("Import secrets from stdin (KEY=value format, one per line)")
130
- .option("--no-redeploy", "Don't trigger redeploy")
131
- .action(async () => {
132
- const projectId = resolveProjectId(program.opts().project);
276
+ .description("Import secrets from stdin (KEY=value, one per line)")
277
+ .option("--global", "Target the whole project")
278
+ .option("-s, --service <name>", "Service to scope to (overrides linked)")
279
+ .option("-p, --project <id>", "Project to scope to")
280
+ .action(async (opts, sub) => {
281
+ const inherited = sub.parent?.opts() || {};
282
+ const scope = await resolveScope(
283
+ opts.project ?? inherited.project,
284
+ opts.service ?? inherited.service,
285
+ opts.global || inherited.global,
286
+ );
133
287
 
134
- // Read stdin
135
288
  const chunks: Buffer[] = [];
136
- for await (const chunk of process.stdin) {
137
- chunks.push(chunk as Buffer);
138
- }
289
+ for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
139
290
  const input = Buffer.concat(chunks).toString("utf-8");
140
291
 
141
292
  const newSecrets: Record<string, string> = {};
142
293
  for (const line of input.split("\n")) {
143
294
  const trimmed = line.trim();
144
295
  if (!trimmed || trimmed.startsWith("#")) continue;
145
- const eqIdx = trimmed.indexOf("=");
146
- if (eqIdx < 1) continue;
147
- newSecrets[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
148
- }
149
-
150
- if (Object.keys(newSecrets).length === 0) {
151
- throw new Error("No valid KEY=value pairs found in input");
296
+ const eq = trimmed.indexOf("=");
297
+ if (eq < 1) continue;
298
+ newSecrets[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
152
299
  }
153
300
 
154
- // Merge with existing
155
- const existing = await api.get<Secret[]>(
156
- `/api/projects/${projectId}/secrets`,
157
- );
158
- const existingMap = new Map(existing.map((s) => [s.key, s.value]));
159
- for (const [k, v] of Object.entries(newSecrets)) {
160
- existingMap.set(k, v);
301
+ if (!Object.keys(newSecrets).length) {
302
+ throw new Error("No valid KEY=value pairs in input");
161
303
  }
162
- const merged = Array.from(existingMap.entries()).map(([key, value]) => ({
163
- key,
164
- value,
165
- }));
166
304
 
167
- await api.put(`/api/projects/${projectId}/secrets`, merged);
305
+ await configApplySecrets(scope, newSecrets);
168
306
 
169
307
  if (isJSONMode()) {
170
- printJSON({ imported: Object.keys(newSecrets) });
308
+ printJSON({ imported: Object.keys(newSecrets), scope: scope.label });
171
309
  } else {
172
- success(`${Object.keys(newSecrets).length} secret(s) imported`);
310
+ success(`${Object.keys(newSecrets).length} ${scope.label} secret(s) imported`);
173
311
  }
174
312
  });
175
313
  }
314
+
315
+ interface VarRef {
316
+ template: string;
317
+ description?: string;
318
+ source?: string;
319
+ }
320
+
321
+ /**
322
+ * Fetch the reference manifest from the backend so users know which
323
+ * `${{...}}` templates are valid (e.g. `${{Postgres.DATABASE_URL}}`,
324
+ * `${{api.LIZARD_PUBLIC_DOMAIN}}`). Backend endpoint:
325
+ * GET /api/projects/<id>/variables:refs
326
+ * GET /api/apps/<id>/variables:refs (for service-scope)
327
+ *
328
+ * Returns a flat list of templates ready to copy-paste into secret values.
329
+ */
330
+ async function printRefs(scope: Scope): Promise<void> {
331
+ const endpoint =
332
+ scope.label === "service" && scope.serviceId
333
+ ? withScope(`/api/apps/${scope.serviceId}/variables:refs`, scope.scope)
334
+ : withScope(`/api/projects/${scope.projectId}/variables:refs`, scope.scope);
335
+
336
+ const refs = await api.get<VarRef[]>(endpoint);
337
+
338
+ if (isJSONMode()) {
339
+ printJSON(refs);
340
+ return;
341
+ }
342
+
343
+ if (!refs.length) {
344
+ console.log("No reference variables exposed in this scope.");
345
+ return;
346
+ }
347
+
348
+ table(
349
+ ["Template", "Source", "Description"],
350
+ refs.map((r) => [chalk.cyan(r.template), r.source || "—", r.description || ""]),
351
+ );
352
+ }