@squadbase/vantage-template 0.0.1

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 (99) hide show
  1. package/README.md +112 -0
  2. package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +162 -0
  3. package/base-template/.squadbase/skills/vantage-app/SKILL.md +120 -0
  4. package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +137 -0
  5. package/base-template/AGENTS.md +339 -0
  6. package/base-template/_404.tsx +18 -0
  7. package/base-template/_error.tsx +14 -0
  8. package/base-template/_gitignore +5 -0
  9. package/base-template/_layout.tsx +60 -0
  10. package/base-template/components/user-card.tsx +132 -0
  11. package/base-template/index.tsx +59 -0
  12. package/base-template/package.json +27 -0
  13. package/base-template/server/api/health.ts +12 -0
  14. package/base-template/squadbase.yml +5 -0
  15. package/base-template/styles.css +18 -0
  16. package/base-template/tsconfig.json +28 -0
  17. package/chart-presets/berry.css +16 -0
  18. package/chart-presets/blue.css +16 -0
  19. package/chart-presets/forest.css +16 -0
  20. package/chart-presets/mono.css +16 -0
  21. package/chart-presets/ocean.css +16 -0
  22. package/chart-presets/sunset.css +16 -0
  23. package/dist/index.js +1157 -0
  24. package/package.json +55 -0
  25. package/ui-templates/CLAUDE.md +200 -0
  26. package/ui-templates/blank/manifest.json +8 -0
  27. package/ui-templates/blank/pages/index.tsx +33 -0
  28. package/ui-templates/blank-ja/manifest.json +8 -0
  29. package/ui-templates/blank-ja/pages/index.tsx +33 -0
  30. package/ui-templates/chart-grid/lib/mock-data.ts +139 -0
  31. package/ui-templates/chart-grid/lib/types.ts +31 -0
  32. package/ui-templates/chart-grid/manifest.json +10 -0
  33. package/ui-templates/chart-grid/pages/index.tsx +331 -0
  34. package/ui-templates/chart-grid/preview-square.png +0 -0
  35. package/ui-templates/chart-grid/preview-wide.png +0 -0
  36. package/ui-templates/chart-grid-ja/lib/mock-data.ts +139 -0
  37. package/ui-templates/chart-grid-ja/lib/types.ts +31 -0
  38. package/ui-templates/chart-grid-ja/manifest.json +10 -0
  39. package/ui-templates/chart-grid-ja/pages/index.tsx +331 -0
  40. package/ui-templates/chart-grid-ja/preview-square.png +0 -0
  41. package/ui-templates/chart-grid-ja/preview-wide.png +0 -0
  42. package/ui-templates/funnel/components/chart-helpers.ts +11 -0
  43. package/ui-templates/funnel/components/stage-table.tsx +63 -0
  44. package/ui-templates/funnel/lib/mock-data.ts +22 -0
  45. package/ui-templates/funnel/lib/types.ts +11 -0
  46. package/ui-templates/funnel/manifest.json +12 -0
  47. package/ui-templates/funnel/pages/index.tsx +252 -0
  48. package/ui-templates/funnel/preview-square.png +0 -0
  49. package/ui-templates/funnel/preview-wide.png +0 -0
  50. package/ui-templates/funnel-ja/components/chart-helpers.ts +11 -0
  51. package/ui-templates/funnel-ja/components/stage-table.tsx +63 -0
  52. package/ui-templates/funnel-ja/lib/mock-data.ts +22 -0
  53. package/ui-templates/funnel-ja/lib/types.ts +11 -0
  54. package/ui-templates/funnel-ja/manifest.json +12 -0
  55. package/ui-templates/funnel-ja/pages/index.tsx +251 -0
  56. package/ui-templates/funnel-ja/preview-square.png +0 -0
  57. package/ui-templates/funnel-ja/preview-wide.png +0 -0
  58. package/ui-templates/kpi-chart-advanced/components/campaign-table.tsx +69 -0
  59. package/ui-templates/kpi-chart-advanced/lib/mock-data.ts +109 -0
  60. package/ui-templates/kpi-chart-advanced/lib/types.ts +38 -0
  61. package/ui-templates/kpi-chart-advanced/manifest.json +11 -0
  62. package/ui-templates/kpi-chart-advanced/pages/index.tsx +339 -0
  63. package/ui-templates/kpi-chart-advanced/preview-square.png +0 -0
  64. package/ui-templates/kpi-chart-advanced/preview-wide.png +0 -0
  65. package/ui-templates/kpi-chart-advanced-ja/components/campaign-table.tsx +69 -0
  66. package/ui-templates/kpi-chart-advanced-ja/lib/mock-data.ts +109 -0
  67. package/ui-templates/kpi-chart-advanced-ja/lib/types.ts +38 -0
  68. package/ui-templates/kpi-chart-advanced-ja/manifest.json +11 -0
  69. package/ui-templates/kpi-chart-advanced-ja/pages/index.tsx +333 -0
  70. package/ui-templates/kpi-chart-advanced-ja/preview-square.png +0 -0
  71. package/ui-templates/kpi-chart-advanced-ja/preview-wide.png +0 -0
  72. package/ui-templates/kpi-chart-simple/components/top-items-table.tsx +73 -0
  73. package/ui-templates/kpi-chart-simple/lib/types.ts +44 -0
  74. package/ui-templates/kpi-chart-simple/manifest.json +11 -0
  75. package/ui-templates/kpi-chart-simple/pages/index.tsx +200 -0
  76. package/ui-templates/kpi-chart-simple/preview-square.png +0 -0
  77. package/ui-templates/kpi-chart-simple/preview-wide.png +0 -0
  78. package/ui-templates/kpi-chart-simple/server/kpi-summary.ts +141 -0
  79. package/ui-templates/kpi-chart-simple-ja/components/top-items-table.tsx +73 -0
  80. package/ui-templates/kpi-chart-simple-ja/lib/types.ts +44 -0
  81. package/ui-templates/kpi-chart-simple-ja/manifest.json +11 -0
  82. package/ui-templates/kpi-chart-simple-ja/pages/index.tsx +200 -0
  83. package/ui-templates/kpi-chart-simple-ja/preview-square.png +0 -0
  84. package/ui-templates/kpi-chart-simple-ja/preview-wide.png +0 -0
  85. package/ui-templates/kpi-chart-simple-ja/server/kpi-summary.ts +141 -0
  86. package/ui-templates/table-focus/components/detail-table.tsx +88 -0
  87. package/ui-templates/table-focus/lib/mock-data.ts +69 -0
  88. package/ui-templates/table-focus/lib/types.ts +24 -0
  89. package/ui-templates/table-focus/manifest.json +11 -0
  90. package/ui-templates/table-focus/pages/index.tsx +180 -0
  91. package/ui-templates/table-focus/preview-square.png +0 -0
  92. package/ui-templates/table-focus/preview-wide.png +0 -0
  93. package/ui-templates/table-focus-ja/components/detail-table.tsx +95 -0
  94. package/ui-templates/table-focus-ja/lib/mock-data.ts +69 -0
  95. package/ui-templates/table-focus-ja/lib/types.ts +24 -0
  96. package/ui-templates/table-focus-ja/manifest.json +11 -0
  97. package/ui-templates/table-focus-ja/pages/index.tsx +180 -0
  98. package/ui-templates/table-focus-ja/preview-square.png +0 -0
  99. package/ui-templates/table-focus-ja/preview-wide.png +0 -0
package/dist/index.js ADDED
@@ -0,0 +1,1157 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { parseArgs } from "util";
5
+
6
+ // src/commands/add.ts
7
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
8
+ import { join as join4 } from "path";
9
+
10
+ // src/ai/customize.ts
11
+ import { readFileSync, writeFileSync } from "fs";
12
+ import { join } from "path";
13
+
14
+ // src/logger.ts
15
+ var colors = {
16
+ red: "\x1B[0;31m",
17
+ green: "\x1B[0;32m",
18
+ yellow: "\x1B[1;33m",
19
+ cyan: "\x1B[0;36m",
20
+ dim: "\x1B[2m",
21
+ reset: "\x1B[0m"
22
+ };
23
+ function log(color, message) {
24
+ console.log(`${colors[color]}${message}${colors.reset}`);
25
+ }
26
+
27
+ // src/ai/diff.ts
28
+ var ANSI = {
29
+ reset: "\x1B[0m",
30
+ red: "\x1B[0;31m",
31
+ green: "\x1B[0;32m",
32
+ cyan: "\x1B[0;36m",
33
+ dim: "\x1B[2m"
34
+ };
35
+ function computeOps(before, after) {
36
+ const n = before.length;
37
+ const m = after.length;
38
+ const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
39
+ for (let i2 = n - 1; i2 >= 0; i2--) {
40
+ for (let j2 = m - 1; j2 >= 0; j2--) {
41
+ if (before[i2] === after[j2]) {
42
+ dp[i2][j2] = dp[i2 + 1][j2 + 1] + 1;
43
+ } else {
44
+ dp[i2][j2] = Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
45
+ }
46
+ }
47
+ }
48
+ const ops = [];
49
+ let i = 0;
50
+ let j = 0;
51
+ while (i < n && j < m) {
52
+ if (before[i] === after[j]) {
53
+ ops.push({ type: "equal", value: before[i] });
54
+ i++;
55
+ j++;
56
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
57
+ ops.push({ type: "delete", value: before[i] });
58
+ i++;
59
+ } else {
60
+ ops.push({ type: "insert", value: after[j] });
61
+ j++;
62
+ }
63
+ }
64
+ while (i < n) ops.push({ type: "delete", value: before[i++] });
65
+ while (j < m) ops.push({ type: "insert", value: after[j++] });
66
+ return ops;
67
+ }
68
+ function unifiedDiff(before, after, filename, options = {}) {
69
+ const beforeLines = before.split("\n");
70
+ const afterLines = after.split("\n");
71
+ const ops = computeOps(beforeLines, afterLines);
72
+ const context = options.context ?? 2;
73
+ const lines = [];
74
+ lines.push(`${ANSI.cyan}--- ${filename}${ANSI.reset}`);
75
+ lines.push(`${ANSI.cyan}+++ ${filename}${ANSI.reset}`);
76
+ const hasChange = ops.some((o) => o.type !== "equal");
77
+ if (!hasChange) {
78
+ lines.push(`${ANSI.dim}(no change)${ANSI.reset}`);
79
+ return lines.join("\n");
80
+ }
81
+ const hunks = [];
82
+ let current = [];
83
+ let trailingEqual = 0;
84
+ for (const op of ops) {
85
+ if (op.type === "equal") {
86
+ trailingEqual++;
87
+ current.push(op);
88
+ if (trailingEqual > context * 2 && current.some((o) => o.type !== "equal")) {
89
+ const trimmed = current.slice(0, current.length - (trailingEqual - context));
90
+ hunks.push(trimmed);
91
+ current = current.slice(current.length - context);
92
+ trailingEqual = context;
93
+ }
94
+ } else {
95
+ trailingEqual = 0;
96
+ current.push(op);
97
+ }
98
+ }
99
+ if (current.some((o) => o.type !== "equal")) {
100
+ hunks.push(current);
101
+ }
102
+ for (const hunk of hunks) {
103
+ let leading = 0;
104
+ while (leading < hunk.length && hunk[leading].type === "equal") leading++;
105
+ if (leading > context) {
106
+ hunk.splice(0, leading - context);
107
+ }
108
+ }
109
+ for (const hunk of hunks) {
110
+ for (const op of hunk) {
111
+ if (op.type === "equal") {
112
+ lines.push(` ${op.value}`);
113
+ } else if (op.type === "delete") {
114
+ lines.push(`${ANSI.red}-${op.value}${ANSI.reset}`);
115
+ } else {
116
+ lines.push(`${ANSI.green}+${op.value}${ANSI.reset}`);
117
+ }
118
+ }
119
+ lines.push(`${ANSI.dim}...${ANSI.reset}`);
120
+ }
121
+ if (lines[lines.length - 1] === `${ANSI.dim}...${ANSI.reset}`) lines.pop();
122
+ return lines.join("\n");
123
+ }
124
+ function diffStats(before, after) {
125
+ const ops = computeOps(before.split("\n"), after.split("\n"));
126
+ let added = 0;
127
+ let removed = 0;
128
+ for (const op of ops) {
129
+ if (op.type === "insert") added++;
130
+ else if (op.type === "delete") removed++;
131
+ }
132
+ return { added, removed };
133
+ }
134
+
135
+ // src/ai/provider.ts
136
+ var PROVIDER_MAP = {
137
+ openai: {
138
+ pkg: "@ai-sdk/openai",
139
+ factory: "createOpenAI",
140
+ defaultModel: "gpt-5.4-mini-2026-03-17",
141
+ envKey: "OPENAI_API_KEY"
142
+ },
143
+ anthropic: {
144
+ pkg: "@ai-sdk/anthropic",
145
+ factory: "createAnthropic",
146
+ defaultModel: "claude-sonnet-4-5",
147
+ envKey: "ANTHROPIC_API_KEY"
148
+ },
149
+ google: {
150
+ pkg: "@ai-sdk/google",
151
+ factory: "createGoogleGenerativeAI",
152
+ defaultModel: "gemini-3.5-flash",
153
+ envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
154
+ envKeyAliases: ["GOOGLE_AI_API_KEY"]
155
+ },
156
+ mistral: {
157
+ pkg: "@ai-sdk/mistral",
158
+ factory: "createMistral",
159
+ defaultModel: "mistral-large-latest",
160
+ envKey: "MISTRAL_API_KEY"
161
+ },
162
+ xai: {
163
+ pkg: "@ai-sdk/xai",
164
+ factory: "createXai",
165
+ defaultModel: "grok-2-latest",
166
+ envKey: "XAI_API_KEY"
167
+ },
168
+ groq: {
169
+ pkg: "@ai-sdk/groq",
170
+ factory: "createGroq",
171
+ defaultModel: "llama-3.3-70b-versatile",
172
+ envKey: "GROQ_API_KEY"
173
+ }
174
+ };
175
+ function normalizeProvider(provider) {
176
+ return provider.toLowerCase().replace(/[-_\s]/g, "");
177
+ }
178
+ async function resolveModel(opts) {
179
+ const normalized = normalizeProvider(opts.provider);
180
+ const entry = PROVIDER_MAP[normalized] ?? {
181
+ pkg: `@ai-sdk/${normalized}`,
182
+ factory: normalized,
183
+ defaultModel: "",
184
+ envKey: `${normalized.toUpperCase()}_API_KEY`
185
+ };
186
+ const apiKey = opts.apiKey ?? process.env[entry.envKey] ?? entry.envKeyAliases?.map((k) => process.env[k]).find((v) => Boolean(v));
187
+ if (!apiKey) {
188
+ const envKeyList = [entry.envKey, ...entry.envKeyAliases ?? []].join(" or ");
189
+ throw new Error(
190
+ `Missing API key for provider "${opts.provider}". Pass --apiKey, or set ${envKeyList}.`
191
+ );
192
+ }
193
+ const modelId = opts.model ?? entry.defaultModel;
194
+ if (!modelId) {
195
+ throw new Error(
196
+ `No default model is known for provider "${opts.provider}". Pass --model <model-id>.`
197
+ );
198
+ }
199
+ let mod;
200
+ try {
201
+ mod = await import(entry.pkg);
202
+ } catch {
203
+ throw new Error(
204
+ [
205
+ `AI SDK provider package "${entry.pkg}" is not installed.`,
206
+ `Run: npm install ai ${entry.pkg}`
207
+ ].join("\n")
208
+ );
209
+ }
210
+ const factory = mod[entry.factory] ?? mod.default?.[entry.factory];
211
+ if (typeof factory !== "function") {
212
+ throw new Error(
213
+ `Could not find factory "${entry.factory}" in "${entry.pkg}". The provider package layout may have changed.`
214
+ );
215
+ }
216
+ const providerInstance = factory({ apiKey, baseURL: opts.baseUrl });
217
+ const modelSettings = normalized === "google" ? { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } : void 0;
218
+ const model = providerInstance(modelId, modelSettings);
219
+ return { model, providerName: normalized, modelId };
220
+ }
221
+
222
+ // src/ai/system-prompt.ts
223
+ var SYSTEM_PROMPT = `You are a copy-alignment assistant for the Squadbase Vantage dashboard template.
224
+
225
+ The files under <FILES_TO_CUSTOMIZE> are a finished, idiomatic dashboard. The user wants only its *surface wording* nudged toward <USER_INTENT> \u2014 just enough that the screen reads as if it were built for that use case. This is a light relabeling pass, not a rebuild. When in doubt, change less.
226
+
227
+ # What you may change
228
+ Only string literals that a user reads on screen or that act as labels:
229
+ - Page / section titles, descriptions, and headings.
230
+ - KPI names, chart titles & descriptions, axis & series & legend text.
231
+ - Filter labels and option text, table column headers.
232
+ - Label lists and copy maps that appear as plain string literals in the provided files \u2014 KPI name lists, stage names, chart axis names, and the like. (Mock data files are not provided; you only relabel the display copy in the component and page files you are given.)
233
+
234
+ # What you must not touch
235
+ You edit only the *text inside* an existing string literal, or the text *between* two existing tags \u2014 never the code around it.
236
+ - Never change a number, currency amount, percentage, or date, and never touch a value that contains code: a \`\${...}\` interpolation, a function call, or a variable reference. Those are sample or computed values, not copy.
237
+ - Never add, remove, reorder, or replace a JSX element, component, icon, or wrapper \u2014 including \`<Placeholder>\` and \`<Skeleton>\`. Do not turn a placeholder into text, or text into a placeholder.
238
+ - Do not edit, add, or remove \`import\` lines, and do not alter logic, types, data shape, array lengths, or component composition.
239
+ - Every edit must leave all existing imports and variables still referenced. If a rename would orphan an import or a variable, skip it.
240
+
241
+ # How to choose wording
242
+ Treat <USER_INTENT> as a hint about the domain, not a spec. Pick natural, concrete labels a real dashboard for that domain would use \u2014 never \`foo / bar / baz\`. Keep each replacement close to the original in length and tone so the layout still fits. If a label already suits the domain, leave it. Files are relabeled independently, so use the same wording for a given concept (e.g. a recurring metric name) everywhere it appears, drawn from <USER_INTENT>, to keep repeated labels consistent across files.
243
+
244
+ # Output
245
+ Return search/replace edits. Each has \`path\` (a dest from the schema enum), \`old_content\` (the exact current snippet, matched byte-for-byte and unique within the file \u2014 include surrounding context if needed), \`new_content\` (the renamed text), and \`rationale\` (one short sentence). You are renaming, so \`new_content\` is never empty. Return an empty array if nothing needs renaming. Use \`notes\` only to flag something noteworthy.
246
+
247
+ Before returning, re-check every edit: it changes display copy only, leaves all code, numbers, and JSX elements intact, and orphans no import or variable.
248
+ `;
249
+
250
+ // src/ai/customize.ts
251
+ async function importAI() {
252
+ try {
253
+ const mod = await import("ai");
254
+ return mod;
255
+ } catch {
256
+ throw new Error(
257
+ [
258
+ `AI SDK package "ai" is not installed.`,
259
+ `Run: npm install ai`
260
+ ].join("\n")
261
+ );
262
+ }
263
+ }
264
+ function buildSchema(allowedPaths) {
265
+ return {
266
+ type: "object",
267
+ additionalProperties: false,
268
+ required: ["edits", "notes"],
269
+ properties: {
270
+ edits: {
271
+ type: "array",
272
+ items: {
273
+ type: "object",
274
+ additionalProperties: false,
275
+ required: ["path", "old_content", "new_content", "rationale"],
276
+ properties: {
277
+ path: { type: "string", enum: allowedPaths },
278
+ old_content: { type: "string" },
279
+ new_content: { type: "string" },
280
+ rationale: { type: "string" }
281
+ }
282
+ }
283
+ },
284
+ notes: { type: "string" }
285
+ }
286
+ };
287
+ }
288
+ function buildUserPrompt(intent, files) {
289
+ const parts = [];
290
+ parts.push(`<USER_INTENT>`);
291
+ parts.push(intent);
292
+ parts.push(`</USER_INTENT>`);
293
+ parts.push("");
294
+ parts.push(`<FILES_TO_CUSTOMIZE>`);
295
+ for (const [path, content] of files) {
296
+ parts.push(`--- FILE: ${path} ---`);
297
+ parts.push(content);
298
+ parts.push("");
299
+ }
300
+ parts.push(`</FILES_TO_CUSTOMIZE>`);
301
+ return parts.join("\n");
302
+ }
303
+ function readInputFiles(projectRoot, templateDir, entries, fromTemplate) {
304
+ const files = /* @__PURE__ */ new Map();
305
+ for (const entry of entries) {
306
+ const path = fromTemplate ? join(templateDir, entry.src) : join(projectRoot, entry.dest);
307
+ try {
308
+ files.set(entry.dest, readFileSync(path, "utf-8"));
309
+ } catch (err) {
310
+ throw new Error(
311
+ `Failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`
312
+ );
313
+ }
314
+ }
315
+ return files;
316
+ }
317
+ function applyEditsToFile(before, edits) {
318
+ let text = before;
319
+ for (const edit of edits) {
320
+ const idx = text.indexOf(edit.old_content);
321
+ if (idx === -1) {
322
+ throw new Error(`old_content not found in file:
323
+ ${edit.old_content}`);
324
+ }
325
+ if (text.indexOf(edit.old_content, idx + 1) !== -1) {
326
+ throw new Error(
327
+ `old_content is not unique (matched more than once); include more surrounding context:
328
+ ${edit.old_content}`
329
+ );
330
+ }
331
+ text = text.slice(0, idx) + edit.new_content + text.slice(idx + edit.old_content.length);
332
+ }
333
+ return text;
334
+ }
335
+ var RELABEL_CONCURRENCY = 4;
336
+ async function mapWithConcurrency(items, limit, fn) {
337
+ const results = new Array(items.length);
338
+ let next = 0;
339
+ const worker = async () => {
340
+ while (true) {
341
+ const i = next++;
342
+ if (i >= items.length) return;
343
+ try {
344
+ results[i] = { status: "fulfilled", value: await fn(items[i], i) };
345
+ } catch (reason) {
346
+ results[i] = { status: "rejected", reason };
347
+ }
348
+ }
349
+ };
350
+ await Promise.all(
351
+ Array.from({ length: Math.min(limit, items.length) }, () => worker())
352
+ );
353
+ return results;
354
+ }
355
+ async function relabelFile(generateObject, jsonSchema, model, intent, path, content) {
356
+ const schema = jsonSchema(buildSchema([path]));
357
+ const userPrompt = buildUserPrompt(intent, /* @__PURE__ */ new Map([[path, content]]));
358
+ const result = await generateObject({
359
+ model,
360
+ schema,
361
+ system: SYSTEM_PROMPT,
362
+ prompt: userPrompt,
363
+ temperature: 0.3,
364
+ maxTokens: 8e3
365
+ });
366
+ const object = result.object;
367
+ if (process.env.SQUADBASE_AI_DEBUG) {
368
+ const r = result;
369
+ log(
370
+ "dim",
371
+ `[debug] ${path} finishReason=${r.finishReason ?? "?"} usage=${JSON.stringify(r.usage ?? {})}`
372
+ );
373
+ }
374
+ let edits = [];
375
+ const rawEdits = object.edits;
376
+ if (typeof rawEdits === "string") {
377
+ edits = parseEditsString(rawEdits);
378
+ } else if (Array.isArray(rawEdits)) {
379
+ edits = rawEdits;
380
+ } else if (rawEdits != null) {
381
+ throw new Error(`AI returned \`edits\` as ${typeof rawEdits}, expected array.`);
382
+ }
383
+ return { edits, notes: object.notes };
384
+ }
385
+ function formatAIError(err, modelId) {
386
+ const e = err;
387
+ const parts = [];
388
+ parts.push(`AI did not return a valid object (model=${modelId}).`);
389
+ if (e.message) parts.push(`Message: ${e.message}`);
390
+ if (e.finishReason) parts.push(`finishReason: ${e.finishReason}`);
391
+ if (typeof e.text === "string" && e.text.length > 0) {
392
+ const snippet = e.text.length > 800 ? `${e.text.slice(0, 800)}\u2026(${e.text.length} chars)` : e.text;
393
+ parts.push(`Raw response text:
394
+ ${snippet}`);
395
+ }
396
+ if (e.cause) {
397
+ const causeMsg = e.cause instanceof Error ? e.cause.message : String(e.cause);
398
+ parts.push(`Cause: ${causeMsg}`);
399
+ }
400
+ if (e.usage) parts.push(`Usage: ${JSON.stringify(e.usage)}`);
401
+ return parts.join("\n");
402
+ }
403
+ async function customizeWithAI(projectRoot, manifest, templateDir, options) {
404
+ const relabelFiles = manifest.files.filter((f) => f.relabel !== false);
405
+ const allowedPaths = relabelFiles.map((f) => f.dest);
406
+ const inputFiles = readInputFiles(
407
+ projectRoot,
408
+ templateDir,
409
+ relabelFiles,
410
+ options.dryRun
411
+ );
412
+ if (!options.json) {
413
+ log("cyan", `
414
+ Customizing with AI (${options.provider}${options.model ? `:${options.model}` : ""})...`);
415
+ log("dim", ` files: ${allowedPaths.length}, intent: ${truncate(options.prompt, 80)}`);
416
+ }
417
+ const { generateObject, jsonSchema } = await importAI();
418
+ const { model, providerName, modelId } = await resolveModel({
419
+ provider: options.provider,
420
+ model: options.model,
421
+ apiKey: options.apiKey,
422
+ baseUrl: options.baseUrl
423
+ });
424
+ const aiError = [];
425
+ const editsArray = [];
426
+ const noteParts = [];
427
+ const settled = await mapWithConcurrency(
428
+ allowedPaths,
429
+ RELABEL_CONCURRENCY,
430
+ (path) => relabelFile(
431
+ generateObject,
432
+ jsonSchema,
433
+ model,
434
+ options.prompt,
435
+ path,
436
+ inputFiles.get(path) ?? ""
437
+ )
438
+ );
439
+ settled.forEach((r, i) => {
440
+ const path = allowedPaths[i];
441
+ if (r.status === "fulfilled") {
442
+ editsArray.push(...r.value.edits);
443
+ if (r.value.notes && r.value.notes.trim().length > 0) {
444
+ noteParts.push(`${path}: ${r.value.notes.trim()}`);
445
+ }
446
+ } else {
447
+ aiError.push(path);
448
+ if (!options.json) {
449
+ const msg = formatAIError(r.reason, modelId);
450
+ log("red", `
451
+ ${path}: AI call failed (left unchanged)`);
452
+ log("dim", ` ${msg.split("\n")[0]}`);
453
+ if (process.env.SQUADBASE_AI_DEBUG) {
454
+ log("dim", msg);
455
+ }
456
+ }
457
+ }
458
+ });
459
+ const allowedSet = new Set(allowedPaths);
460
+ const skipped = [];
461
+ const editsByPath = /* @__PURE__ */ new Map();
462
+ for (const raw of editsArray) {
463
+ if (!allowedSet.has(raw.path)) {
464
+ skipped.push(raw.path);
465
+ continue;
466
+ }
467
+ const arr = editsByPath.get(raw.path) ?? [];
468
+ arr.push(raw);
469
+ editsByPath.set(raw.path, arr);
470
+ }
471
+ const edits = [];
472
+ const failedVerification = [];
473
+ for (const [path, rawForPath] of editsByPath) {
474
+ const before = inputFiles.get(path) ?? "";
475
+ try {
476
+ const after = applyEditsToFile(before, rawForPath);
477
+ if (after === before) continue;
478
+ const { added, removed } = diffStats(before, after);
479
+ const rationale = rawForPath.map((e) => e.rationale).filter((s) => s && s.trim().length > 0).join("; ");
480
+ edits.push({
481
+ path,
482
+ rationale,
483
+ before,
484
+ after,
485
+ added,
486
+ removed
487
+ });
488
+ } catch (err) {
489
+ const msg = err instanceof Error ? err.message : String(err);
490
+ failedVerification.push(path);
491
+ if (!options.json) {
492
+ log("red", `
493
+ ${path}: edit verification failed`);
494
+ log("dim", ` ${msg.split("\n")[0]}`);
495
+ if (process.env.SQUADBASE_AI_DEBUG) {
496
+ log("dim", msg);
497
+ }
498
+ }
499
+ }
500
+ }
501
+ const editedPaths = new Set(edits.map((e) => e.path));
502
+ const failedSet = new Set(failedVerification);
503
+ const erroredSet = new Set(aiError);
504
+ const unchanged = allowedPaths.filter(
505
+ (p) => !editedPaths.has(p) && !failedSet.has(p) && !erroredSet.has(p)
506
+ );
507
+ if (options.dryRun) {
508
+ if (!options.json) {
509
+ for (const edit of edits) {
510
+ log("cyan", `
511
+ ${edit.path} (+${edit.added} -${edit.removed})`);
512
+ if (edit.rationale) log("dim", ` ${edit.rationale}`);
513
+ console.log(unifiedDiff(edit.before, edit.after, edit.path));
514
+ }
515
+ }
516
+ } else {
517
+ for (const edit of edits) {
518
+ writeFileSync(join(projectRoot, edit.path), edit.after, "utf-8");
519
+ if (!options.json) {
520
+ log("cyan", ` rewrote: ${edit.path} (+${edit.added} -${edit.removed})`);
521
+ }
522
+ }
523
+ }
524
+ return {
525
+ edits,
526
+ unchanged,
527
+ skipped,
528
+ failedVerification,
529
+ aiError,
530
+ notes: noteParts.length > 0 ? noteParts.join("\n") : void 0,
531
+ provider: providerName,
532
+ model: modelId
533
+ };
534
+ }
535
+ function truncate(s, n) {
536
+ if (s.length <= n) return s;
537
+ return s.slice(0, n - 1) + "\u2026";
538
+ }
539
+ function stripTrailingCommas(s) {
540
+ const out = [];
541
+ let inString = false;
542
+ let escape = false;
543
+ for (let i = 0; i < s.length; i++) {
544
+ const ch = s[i];
545
+ if (inString) {
546
+ out.push(ch);
547
+ if (escape) {
548
+ escape = false;
549
+ } else if (ch === "\\") {
550
+ escape = true;
551
+ } else if (ch === '"') {
552
+ inString = false;
553
+ }
554
+ continue;
555
+ }
556
+ if (ch === '"') {
557
+ inString = true;
558
+ out.push(ch);
559
+ continue;
560
+ }
561
+ if (ch === ",") {
562
+ let j = i + 1;
563
+ while (j < s.length && /\s/.test(s[j])) j++;
564
+ if (j < s.length && (s[j] === "]" || s[j] === "}")) {
565
+ continue;
566
+ }
567
+ }
568
+ out.push(ch);
569
+ }
570
+ return out.join("");
571
+ }
572
+ function parseEditsString(raw) {
573
+ let lastErr = null;
574
+ for (const candidate of [raw, stripTrailingCommas(raw)]) {
575
+ try {
576
+ const parsed = JSON.parse(candidate);
577
+ if (Array.isArray(parsed)) return parsed;
578
+ throw new Error(`Parsed edits is ${typeof parsed}, expected array.`);
579
+ } catch (err) {
580
+ lastErr = err;
581
+ }
582
+ }
583
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
584
+ }
585
+
586
+ // src/apply.ts
587
+ import { copyFileSync, existsSync as existsSync2, mkdirSync } from "fs";
588
+ import { dirname as dirname2, join as join3 } from "path";
589
+
590
+ // src/manifest.ts
591
+ import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
592
+ import { dirname, join as join2 } from "path";
593
+ import { fileURLToPath } from "url";
594
+ var __dirname = dirname(fileURLToPath(import.meta.url));
595
+ function getTemplatesDir(source = "ui-templates") {
596
+ return join2(__dirname, "..", source);
597
+ }
598
+ function getTemplateDir(templateName, source = "ui-templates") {
599
+ return join2(getTemplatesDir(source), templateName);
600
+ }
601
+ function listTemplateNames(source = "ui-templates") {
602
+ const templatesDir = getTemplatesDir(source);
603
+ if (!existsSync(templatesDir)) {
604
+ return [];
605
+ }
606
+ return readdirSync(templatesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
607
+ }
608
+ function loadManifest(templateName, source = "ui-templates") {
609
+ const templateDir = getTemplateDir(templateName, source);
610
+ if (!existsSync(templateDir)) {
611
+ throw new Error(`Template "${templateName}" not found.`);
612
+ }
613
+ const manifestPath = join2(templateDir, "manifest.json");
614
+ if (!existsSync(manifestPath)) {
615
+ throw new Error(`Template "${templateName}" is missing manifest.json.`);
616
+ }
617
+ const raw = JSON.parse(readFileSync2(manifestPath, "utf-8"));
618
+ if (!raw.name || !raw.files || !Array.isArray(raw.files)) {
619
+ throw new Error(`Invalid manifest.json in template "${templateName}".`);
620
+ }
621
+ return raw;
622
+ }
623
+
624
+ // src/apply.ts
625
+ function applyTemplate(projectRoot, manifest, options) {
626
+ const templateDir = getTemplateDir(manifest.name, options.source ?? "ui-templates");
627
+ const conflicts = checkConflicts(projectRoot, manifest.files, options.force);
628
+ if (conflicts.length > 0) {
629
+ log("red", "File conflicts detected (use --force to overwrite):");
630
+ for (const c of conflicts) {
631
+ log("red", ` ${c}`);
632
+ }
633
+ process.exit(1);
634
+ }
635
+ for (const file of manifest.files) {
636
+ const src = join3(templateDir, file.src);
637
+ const dest = join3(projectRoot, file.dest);
638
+ if (options.dryRun) {
639
+ const label2 = file.action === "replace" ? "replace" : "add";
640
+ log("dim", ` [dry-run] ${label2}: ${file.dest}`);
641
+ continue;
642
+ }
643
+ mkdirSync(dirname2(dest), { recursive: true });
644
+ copyFileSync(src, dest);
645
+ const label = file.action === "replace" ? "replaced" : "added";
646
+ log("cyan", ` ${label}: ${file.dest}`);
647
+ }
648
+ }
649
+ function checkConflicts(projectRoot, files, force) {
650
+ if (force) return [];
651
+ return files.filter((f) => f.action === "add" && existsSync2(join3(projectRoot, f.dest))).map((f) => f.dest);
652
+ }
653
+
654
+ // src/commands/add.ts
655
+ function isVantageProject(projectRoot) {
656
+ const pkgPath = join4(projectRoot, "package.json");
657
+ if (!existsSync3(pkgPath)) return false;
658
+ try {
659
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
660
+ return Boolean(pkg.dependencies?.["@squadbase/vantage"] ?? pkg.devDependencies?.["@squadbase/vantage"]);
661
+ } catch {
662
+ return false;
663
+ }
664
+ }
665
+ function findStaleTemplateFiles(projectRoot, appliedTemplate, source) {
666
+ const stale = [];
667
+ for (const name of listTemplateNames(source)) {
668
+ if (name === appliedTemplate) continue;
669
+ if (name.replace(/-ja$/, "") === appliedTemplate.replace(/-ja$/, "")) continue;
670
+ let manifest;
671
+ try {
672
+ manifest = loadManifest(name, source);
673
+ } catch {
674
+ continue;
675
+ }
676
+ for (const file of manifest.files) {
677
+ if (file.action !== "add") continue;
678
+ if (existsSync3(join4(projectRoot, file.dest))) stale.push(file.dest);
679
+ }
680
+ }
681
+ return [...new Set(stale)].sort();
682
+ }
683
+ async function addTemplate(templateName, options) {
684
+ const projectRoot = process.cwd();
685
+ const source = options.source ?? "ui-templates";
686
+ if (!isVantageProject(projectRoot)) {
687
+ log("red", "@squadbase/vantage is not a dependency here. Are you in a Squadbase Vantage project?");
688
+ log("dim", "Run 'npx @squadbase/vantage-template init' first.");
689
+ process.exit(1);
690
+ }
691
+ const available = listTemplateNames(source);
692
+ if (!available.includes(templateName)) {
693
+ log("red", `Template "${templateName}" not found in ${source}/.`);
694
+ if (available.length > 0) {
695
+ log("yellow", `Available templates: ${available.join(", ")}`);
696
+ }
697
+ process.exit(1);
698
+ }
699
+ const manifest = loadManifest(templateName, source);
700
+ const templateDir = getTemplateDir(templateName, source);
701
+ if (!options.ai?.json) {
702
+ log("green", `Applying template: ${manifest.name}`);
703
+ if (options.dryRun) {
704
+ log("yellow", "(dry run \u2014 no files will be changed)");
705
+ }
706
+ const entry = manifest.files.find((f) => f.action === "replace");
707
+ if (entry && existsSync3(join4(projectRoot, entry.dest))) {
708
+ log("yellow", ` ${entry.dest} will be overwritten \u2014 commit first if it holds your work.`);
709
+ }
710
+ }
711
+ applyTemplate(projectRoot, manifest, options);
712
+ if (!options.ai?.json) {
713
+ const stale = findStaleTemplateFiles(projectRoot, templateName, source);
714
+ if (stale.length > 0) {
715
+ log("yellow", "\nLeftovers from a previously applied template (nothing imports them):");
716
+ for (const dest of stale) log("dim", ` ${dest}`);
717
+ log("dim", " Delete them so they stop showing up in searches.");
718
+ }
719
+ }
720
+ if (options.ai) {
721
+ const result = await customizeWithAI(projectRoot, manifest, templateDir, options.ai);
722
+ if (options.ai.json) {
723
+ console.log(
724
+ JSON.stringify(
725
+ {
726
+ provider: result.provider,
727
+ model: result.model,
728
+ edits: result.edits.map((e) => ({
729
+ path: e.path,
730
+ rationale: e.rationale,
731
+ added: e.added,
732
+ removed: e.removed
733
+ })),
734
+ unchanged: result.unchanged,
735
+ skipped: result.skipped,
736
+ failedVerification: result.failedVerification,
737
+ aiError: result.aiError,
738
+ notes: result.notes,
739
+ dryRun: options.dryRun
740
+ },
741
+ null,
742
+ 2
743
+ )
744
+ );
745
+ return;
746
+ }
747
+ log("green", `
748
+ AI customization complete (${result.provider}:${result.model})`);
749
+ log(
750
+ "dim",
751
+ ` edited: ${result.edits.length}, unchanged: ${result.unchanged.length}, skipped: ${result.skipped.length}, failed: ${result.failedVerification.length}, aiError: ${result.aiError.length}`
752
+ );
753
+ if (result.skipped.length > 0) {
754
+ log("yellow", ` skipped paths (not in manifest): ${result.skipped.join(", ")}`);
755
+ }
756
+ if (result.failedVerification.length > 0) {
757
+ log("yellow", ` failed verification (left unchanged): ${result.failedVerification.join(", ")}`);
758
+ }
759
+ if (result.aiError.length > 0) {
760
+ log("yellow", ` AI call failed (left unchanged): ${result.aiError.join(", ")}`);
761
+ }
762
+ if (result.notes) {
763
+ log("cyan", `
764
+ Notes from AI:`);
765
+ log("dim", ` ${result.notes}`);
766
+ }
767
+ if (!options.dryRun && result.edits.length > 0) {
768
+ log("dim", `
769
+ Run "git diff" to review the AI changes.`);
770
+ }
771
+ return;
772
+ }
773
+ if (!options.dryRun) {
774
+ log("green", "\nTemplate applied successfully!");
775
+ log("dim", 'Run "npx vantage check" to confirm the project still builds.');
776
+ }
777
+ }
778
+
779
+ // src/commands/chart.ts
780
+ import { existsSync as existsSync5 } from "fs";
781
+ import { join as join6 } from "path";
782
+
783
+ // src/chart-presets.ts
784
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
785
+ import { dirname as dirname3, join as join5 } from "path";
786
+ import { fileURLToPath as fileURLToPath2 } from "url";
787
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
788
+ var PRESET_DESCRIPTIONS = {
789
+ blue: "Default \u2014 blue, teal, fuchsia, violet, gray",
790
+ sunset: "Warm \u2014 orange, rose, amber, pink, stone",
791
+ forest: "Natural \u2014 emerald, lime, teal, green, stone",
792
+ ocean: "Cool \u2014 sky, cyan, indigo, teal, slate",
793
+ berry: "Vibrant \u2014 purple, pink, fuchsia, violet, rose",
794
+ mono: "Minimal \u2014 zinc monochrome"
795
+ };
796
+ var BEGIN_MARKER = "/* vantage-template:chart-preset \u2014 regenerated by `chart`; edits here are lost */";
797
+ var END_MARKER = "/* vantage-template:chart-preset:end */";
798
+ function getChartPresetsDir() {
799
+ return join5(__dirname2, "..", "chart-presets");
800
+ }
801
+ function listChartPresetNames() {
802
+ const dir = getChartPresetsDir();
803
+ if (!existsSync4(dir)) return [];
804
+ return readdirSync2(dir).filter((f) => f.endsWith(".css")).map((f) => f.replace(/\.css$/, "")).sort();
805
+ }
806
+ function getChartPresetDescription(name) {
807
+ return PRESET_DESCRIPTIONS[name] ?? "";
808
+ }
809
+ function getChartPresetPath(name) {
810
+ return join5(getChartPresetsDir(), `${name}.css`);
811
+ }
812
+ function applyChartPreset(projectRoot, presetName, options = {}) {
813
+ const presetPath = getChartPresetPath(presetName);
814
+ if (!existsSync4(presetPath)) {
815
+ throw new Error(`Chart preset "${presetName}" not found.`);
816
+ }
817
+ const targetPath = join5(projectRoot, "styles.css");
818
+ if (!existsSync4(targetPath)) {
819
+ throw new Error("styles.css not found in project.");
820
+ }
821
+ const preset = readFileSync4(presetPath, "utf-8").trimEnd();
822
+ const block = `${BEGIN_MARKER}
823
+ ${preset}
824
+ ${END_MARKER}
825
+ `;
826
+ const current = readFileSync4(targetPath, "utf-8");
827
+ const begin = current.indexOf(BEGIN_MARKER);
828
+ const end = current.indexOf(END_MARKER);
829
+ let content;
830
+ if (begin !== -1 && end !== -1) {
831
+ content = current.slice(0, begin) + block + current.slice(end + END_MARKER.length + 1);
832
+ } else {
833
+ content = `${current.trimEnd()}
834
+
835
+ ${block}`;
836
+ }
837
+ if (!options.dryRun) {
838
+ writeFileSync2(targetPath, content, "utf-8");
839
+ }
840
+ return { targetPath, content };
841
+ }
842
+
843
+ // src/commands/chart.ts
844
+ function setChartPreset(presetName, options) {
845
+ const projectRoot = process.cwd();
846
+ const stylesPath = join6(projectRoot, "styles.css");
847
+ if (!existsSync5(stylesPath)) {
848
+ log("red", "styles.css not found. Are you in a Squadbase Vantage project?");
849
+ process.exit(1);
850
+ }
851
+ const available = listChartPresetNames();
852
+ if (!available.includes(presetName)) {
853
+ log("red", `Chart preset "${presetName}" not found.`);
854
+ if (available.length > 0) {
855
+ log("yellow", `Available presets: ${available.join(", ")}`);
856
+ }
857
+ process.exit(1);
858
+ }
859
+ log("green", `Applying chart preset: ${presetName}`);
860
+ const description = getChartPresetDescription(presetName);
861
+ if (description) {
862
+ log("dim", ` ${description}`);
863
+ }
864
+ if (options.dryRun) {
865
+ log("yellow", "(dry run \u2014 no files will be changed)");
866
+ const { content } = applyChartPreset(projectRoot, presetName, { dryRun: true });
867
+ log("dim", "\n--- styles.css (preview) ---");
868
+ console.log(content);
869
+ return;
870
+ }
871
+ applyChartPreset(projectRoot, presetName);
872
+ log("cyan", " updated: styles.css");
873
+ log("green", "\nChart preset applied successfully!");
874
+ }
875
+
876
+ // src/commands/init.ts
877
+ import { spawnSync } from "child_process";
878
+ import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync3 } from "fs";
879
+ import { dirname as dirname4, join as join7 } from "path";
880
+ import { fileURLToPath as fileURLToPath3 } from "url";
881
+ var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
882
+ var RENAME_ON_COPY = {
883
+ _gitignore: ".gitignore"
884
+ };
885
+ function getBaseTemplateDir() {
886
+ return join7(__dirname3, "..", "base-template");
887
+ }
888
+ function initProject(options) {
889
+ const targetDir = process.cwd();
890
+ const baseTemplateDir = getBaseTemplateDir();
891
+ if (!existsSync6(baseTemplateDir)) {
892
+ log("red", "Base template not found. The package may be corrupted.");
893
+ process.exit(1);
894
+ }
895
+ if (options.chart) {
896
+ const available = listChartPresetNames();
897
+ if (!available.includes(options.chart)) {
898
+ log("red", `Chart preset "${options.chart}" not found.`);
899
+ if (available.length > 0) {
900
+ log("yellow", `Available presets: ${available.join(", ")}`);
901
+ }
902
+ process.exit(1);
903
+ }
904
+ }
905
+ if (!options.force && existsSync6(join7(targetDir, "squadbase.yml"))) {
906
+ log("red", "This directory already contains a Squadbase project (squadbase.yml exists).");
907
+ log("yellow", "Use --force to overwrite.");
908
+ process.exit(1);
909
+ }
910
+ if (!options.force && existsSync6(join7(targetDir, "index.tsx"))) {
911
+ log("red", "This directory already contains an index.tsx.");
912
+ log("yellow", "Use --force to overwrite.");
913
+ process.exit(1);
914
+ }
915
+ log("green", "Initializing Squadbase Vantage project...");
916
+ const entries = readdirSync3(baseTemplateDir, { withFileTypes: true });
917
+ for (const entry of entries) {
918
+ const destName = RENAME_ON_COPY[entry.name] ?? entry.name;
919
+ const src = join7(baseTemplateDir, entry.name);
920
+ const dest = join7(targetDir, destName);
921
+ if (entry.isDirectory()) {
922
+ mkdirSync2(dest, { recursive: true });
923
+ cpSync(src, dest, { recursive: true, force: options.force });
924
+ } else {
925
+ if (!options.force && existsSync6(dest)) {
926
+ log("yellow", ` skipped (exists): ${destName}`);
927
+ continue;
928
+ }
929
+ cpSync(src, dest);
930
+ }
931
+ log("cyan", ` ${destName}`);
932
+ }
933
+ if (options.chart) {
934
+ applyChartPreset(targetDir, options.chart);
935
+ log("cyan", ` chart preset: ${options.chart}`);
936
+ }
937
+ if (options.skipInstall) {
938
+ log("green", "\nProject initialized successfully!");
939
+ log("dim", "\nNext steps:");
940
+ log("dim", " npm install");
941
+ log("dim", " npm run dev");
942
+ return;
943
+ }
944
+ log("green", "\nInstalling dependencies...");
945
+ const install = spawnSync("npm", ["install"], {
946
+ cwd: targetDir,
947
+ stdio: "inherit",
948
+ shell: process.platform === "win32"
949
+ });
950
+ if (install.error || install.status !== 0) {
951
+ log("red", "\nDependency installation failed.");
952
+ log("yellow", "Run 'npm install' manually in the project directory.");
953
+ process.exit(1);
954
+ }
955
+ log("green", "\nProject initialized successfully!");
956
+ log("dim", "\nNext steps:");
957
+ log("dim", " npm run dev");
958
+ }
959
+
960
+ // src/commands/list.ts
961
+ var PREVIEW_BRANCH = "main";
962
+ function previewUrls(templateName, source) {
963
+ const base = `https://raw.githubusercontent.com/squadbase/squadbase-templates/refs/heads/${PREVIEW_BRANCH}/vantage-template/${source}`;
964
+ return {
965
+ image: `${base}/${templateName}/preview-wide.png`,
966
+ imageSquare: `${base}/${templateName}/preview-square.png`
967
+ };
968
+ }
969
+ function listTemplates(options = {}) {
970
+ const source = "ui-templates";
971
+ const names = listTemplateNames(source);
972
+ const isJa = options.lang === "ja";
973
+ const filtered = names.filter((name) => isJa ? name.endsWith("-ja") : !name.endsWith("-ja"));
974
+ if (options.json) {
975
+ const items = [];
976
+ for (const name of filtered) {
977
+ try {
978
+ const manifest = loadManifest(name, source);
979
+ items.push({
980
+ name: manifest.name,
981
+ description: manifest.description,
982
+ ...previewUrls(name, source)
983
+ });
984
+ } catch {
985
+ }
986
+ }
987
+ console.log(JSON.stringify(items, null, 2));
988
+ return;
989
+ }
990
+ if (filtered.length === 0) {
991
+ log("yellow", "No UI templates available.");
992
+ } else {
993
+ log("green", "Available UI templates:");
994
+ for (const name of filtered) {
995
+ try {
996
+ const manifest = loadManifest(name, source);
997
+ const urls = previewUrls(name, source);
998
+ log("cyan", ` ${manifest.name} \u2014 ${manifest.description}`);
999
+ log("dim", ` image: ${urls.image}`);
1000
+ log("dim", ` imageSquare: ${urls.imageSquare}`);
1001
+ } catch {
1002
+ log("dim", ` ${name} (invalid manifest)`);
1003
+ }
1004
+ }
1005
+ }
1006
+ const presets = listChartPresetNames();
1007
+ if (presets.length > 0) {
1008
+ log("green", "\nAvailable chart presets:");
1009
+ for (const name of presets) {
1010
+ const description = getChartPresetDescription(name);
1011
+ if (description) {
1012
+ log("cyan", ` ${name} \u2014 ${description}`);
1013
+ } else {
1014
+ log("cyan", ` ${name}`);
1015
+ }
1016
+ }
1017
+ }
1018
+ }
1019
+
1020
+ // src/index.ts
1021
+ var HELP = `
1022
+ Usage: npx @squadbase/vantage-template <command> [options]
1023
+
1024
+ Commands:
1025
+ init Initialize a new Squadbase Vantage project
1026
+ add <template-name> Apply a UI template to the current project
1027
+ chart <preset-name> Switch the chart color preset
1028
+ list List available templates and chart presets
1029
+
1030
+ Options:
1031
+ --force Overwrite existing files
1032
+ --dry-run Show what would be done without making changes
1033
+ --skip-install Skip dependency installation after init
1034
+ --chart <preset> Apply a chart preset during init (e.g. --chart sunset)
1035
+ --json Output as JSON (for the list / add commands)
1036
+ --lang <code> Filter list by language (e.g. --lang ja)
1037
+ --help Show this help message
1038
+
1039
+ AI customization (add only):
1040
+ --prompt <text> Customize the applied template with AI. Triggers AI mode.
1041
+ --provider <name> AI provider name (openai, anthropic, google, mistral, xai, groq, ...)
1042
+ --model <id> Model id. Defaults per provider (e.g. gpt-5.4-mini-2026-03-17, claude-sonnet-4-5).
1043
+ --apiKey <key> API key. Falls back to provider-specific env var if omitted.
1044
+ --env-file <path> Load env vars from a .env file before invoking AI (existing process.env wins).
1045
+ --base-url <url> Optional OpenAI-compatible endpoint.
1046
+
1047
+ Examples:
1048
+ npx @squadbase/vantage-template init
1049
+ npx @squadbase/vantage-template list --lang ja
1050
+ npx @squadbase/vantage-template add kpi-chart-simple \\
1051
+ --prompt "SaaS MRR/ARR dashboard" --provider openai --apiKey $OPENAI_API_KEY
1052
+ npx @squadbase/vantage-template add funnel --prompt "..." \\
1053
+ --provider anthropic --apiKey $ANTHROPIC_API_KEY --dry-run
1054
+ `.trim();
1055
+ async function main() {
1056
+ const { values, positionals } = parseArgs({
1057
+ options: {
1058
+ force: { type: "boolean", default: false },
1059
+ "dry-run": { type: "boolean", default: false },
1060
+ "skip-install": { type: "boolean", default: false },
1061
+ chart: { type: "string" },
1062
+ json: { type: "boolean", default: false },
1063
+ lang: { type: "string" },
1064
+ help: { type: "boolean", default: false },
1065
+ prompt: { type: "string" },
1066
+ provider: { type: "string" },
1067
+ model: { type: "string" },
1068
+ apiKey: { type: "string" },
1069
+ "base-url": { type: "string" },
1070
+ "env-file": { type: "string" }
1071
+ },
1072
+ allowPositionals: true,
1073
+ strict: true
1074
+ });
1075
+ if (values.help || positionals.length === 0) {
1076
+ console.log(HELP);
1077
+ return;
1078
+ }
1079
+ const command = positionals[0];
1080
+ if (command === "init") {
1081
+ initProject({
1082
+ force: values.force ?? false,
1083
+ skipInstall: values["skip-install"] ?? false,
1084
+ chart: values.chart
1085
+ });
1086
+ } else if (command === "list") {
1087
+ listTemplates({
1088
+ json: values.json ?? false,
1089
+ lang: values.lang
1090
+ });
1091
+ } else if (command === "add") {
1092
+ const templateName = positionals[1];
1093
+ if (!templateName) {
1094
+ log("red", "Missing template name.");
1095
+ log("dim", "Usage: npx @squadbase/vantage-template add <template-name>");
1096
+ process.exit(1);
1097
+ }
1098
+ const envFile = values["env-file"];
1099
+ if (envFile !== void 0) {
1100
+ if (!values.prompt) {
1101
+ log("red", "--env-file requires --prompt (it only affects AI customization).");
1102
+ process.exit(1);
1103
+ }
1104
+ if (typeof process.loadEnvFile !== "function") {
1105
+ log("red", "--env-file requires Node.js 20.12.0 or later (process.loadEnvFile).");
1106
+ log("dim", `Current version: ${process.version}`);
1107
+ process.exit(1);
1108
+ }
1109
+ try {
1110
+ process.loadEnvFile(envFile);
1111
+ } catch (err) {
1112
+ const reason = err instanceof Error ? err.message : String(err);
1113
+ log("red", `Failed to load env file "${envFile}": ${reason}`);
1114
+ process.exit(1);
1115
+ }
1116
+ }
1117
+ const ai = values.prompt ? (() => {
1118
+ if (!values.provider) {
1119
+ log("red", "--prompt requires --provider (e.g. --provider openai).");
1120
+ process.exit(1);
1121
+ }
1122
+ return {
1123
+ prompt: values.prompt,
1124
+ provider: values.provider,
1125
+ model: values.model,
1126
+ apiKey: values.apiKey,
1127
+ baseUrl: values["base-url"],
1128
+ dryRun: values["dry-run"] ?? false,
1129
+ json: values.json ?? false
1130
+ };
1131
+ })() : void 0;
1132
+ await addTemplate(templateName, {
1133
+ force: values.force ?? false,
1134
+ dryRun: values["dry-run"] ?? false,
1135
+ source: "ui-templates",
1136
+ ai
1137
+ });
1138
+ } else if (command === "chart") {
1139
+ const presetName = positionals[1];
1140
+ if (!presetName) {
1141
+ log("red", "Missing chart preset name.");
1142
+ log("dim", "Usage: npx @squadbase/vantage-template chart <preset-name>");
1143
+ process.exit(1);
1144
+ }
1145
+ setChartPreset(presetName, {
1146
+ dryRun: values["dry-run"] ?? false
1147
+ });
1148
+ } else {
1149
+ log("red", `Unknown command: "${command}"`);
1150
+ console.log(HELP);
1151
+ process.exit(1);
1152
+ }
1153
+ }
1154
+ main().catch((err) => {
1155
+ log("red", err instanceof Error ? err.message : String(err));
1156
+ process.exit(1);
1157
+ });