@alexkroman1/aai-cli 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_api-client-H4MFOr8j.mjs +35 -0
- package/dist/_bundler-BjgPrg7s.mjs +118 -0
- package/dist/{_discover-CbHCotwB.mjs → _discover-a8yIuqEp.mjs} +50 -30
- package/dist/_init-VzVTpJFZ.mjs +56 -0
- package/dist/_templates-CtZBILce.mjs +72 -0
- package/dist/_ui-DWGXImbO.mjs +30 -0
- package/dist/cli.mjs +57 -260
- package/dist/delete-BvRel3Tw.mjs +34 -0
- package/dist/deploy-ChUnIpu5.mjs +92 -0
- package/dist/dev-DEZKrw8v.mjs +18 -0
- package/dist/init-CNoNUGRN.mjs +113 -0
- package/dist/secret-DLosn47q.mjs +47 -0
- package/dist/{test-9fFn9DCf.mjs → test-CYoqKJP0.mjs} +8 -10
- package/package.json +7 -14
- package/dist/_build-CElA0PJB.mjs +0 -176
- package/dist/_init-CQ6Cw4k-.mjs +0 -108
- package/dist/_link-D6mmDQ0B.mjs +0 -49
- package/dist/_server-common-DekUfYGG.mjs +0 -105
- package/dist/_ui-C9IvR7Fh.mjs +0 -50
- package/dist/delete-DYUD8j8f.mjs +0 -40
- package/dist/deploy-BWF0H8UK.mjs +0 -90
- package/dist/dev-nCt6xqmm.mjs +0 -60
- package/dist/doctor-Dvz0rdWo.mjs +0 -215
- package/dist/generate-Di_Shw-X.mjs +0 -393
- package/dist/init-Pa-YbNnJ.mjs +0 -61
- package/dist/rag-qtN4p79Q.mjs +0 -178
- package/dist/secret-BkN22eT1.mjs +0 -50
- package/dist/start-BDAtcDr3.mjs +0 -23
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//#region _api-client.ts
|
|
3
|
+
/**
|
|
4
|
+
* Shared HTTP helpers for platform API calls (deploy, delete, secrets).
|
|
5
|
+
*/
|
|
6
|
+
const HINT_INVALID_API_KEY = "Your API key may be invalid. Check ~/.config/aai/config.json or set ASSEMBLYAI_API_KEY.";
|
|
7
|
+
/**
|
|
8
|
+
* Send an authenticated request to the platform API.
|
|
9
|
+
*
|
|
10
|
+
* Adds the `Authorization` header and, on network failure, throws with a
|
|
11
|
+
* contextual hint (localhost → "is the dev server running?", remote →
|
|
12
|
+
* "check your network connection").
|
|
13
|
+
*/
|
|
14
|
+
async function apiRequest(url, init, fetchFn = globalThis.fetch.bind(globalThis)) {
|
|
15
|
+
const { apiKey, action, ...rest } = init;
|
|
16
|
+
const headers = {
|
|
17
|
+
Authorization: `Bearer ${apiKey}`,
|
|
18
|
+
...rest.body ? { "Content-Type": "application/json" } : {},
|
|
19
|
+
...rest.headers
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
return await fetchFn(url, {
|
|
23
|
+
...rest,
|
|
24
|
+
headers
|
|
25
|
+
});
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error(`${action} failed: could not reach ${url}\n Check your network connection and verify the server URL is correct.`, { cause: err });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Format a non-ok API response into a descriptive error. */
|
|
31
|
+
function apiError(action, status, body, hint) {
|
|
32
|
+
return /* @__PURE__ */ new Error(`${action} failed (HTTP ${status}): ${body}${hint ? `\n ${hint}` : ""}`);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { apiError as n, apiRequest as r, HINT_INVALID_API_KEY as t };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { errorMessage } from "@alexkroman1/aai/utils";
|
|
5
|
+
import { build } from "vite";
|
|
6
|
+
//#region _bundler.ts
|
|
7
|
+
var BundleError = class extends Error {
|
|
8
|
+
constructor(message, options) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.name = "BundleError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const TEXT_EXTENSIONS = new Set([
|
|
14
|
+
".html",
|
|
15
|
+
".htm",
|
|
16
|
+
".css",
|
|
17
|
+
".js",
|
|
18
|
+
".mjs",
|
|
19
|
+
".cjs",
|
|
20
|
+
".ts",
|
|
21
|
+
".mts",
|
|
22
|
+
".json",
|
|
23
|
+
".map",
|
|
24
|
+
".svg",
|
|
25
|
+
".xml",
|
|
26
|
+
".txt",
|
|
27
|
+
".md"
|
|
28
|
+
]);
|
|
29
|
+
async function readDirFiles(dir) {
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = await fs.readdir(dir, {
|
|
33
|
+
recursive: true,
|
|
34
|
+
withFileTypes: true
|
|
35
|
+
});
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") return {};
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
const files = {};
|
|
41
|
+
await Promise.all(entries.filter((e) => e.isFile()).map(async (e) => {
|
|
42
|
+
const full = path.join(e.parentPath, e.name);
|
|
43
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
44
|
+
const rel = path.relative(dir, full);
|
|
45
|
+
if (TEXT_EXTENSIONS.has(ext)) files[rel] = await fs.readFile(full, "utf-8");
|
|
46
|
+
else files[rel] = `base64:${(await fs.readFile(full)).toString("base64")}`;
|
|
47
|
+
}));
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Bundle an agent project using Vite.
|
|
52
|
+
*
|
|
53
|
+
* - Worker: `vite build --ssr agent.ts` (uses project's vite.config.ts)
|
|
54
|
+
* - Client: `vite build` (uses project's vite.config.ts)
|
|
55
|
+
*/
|
|
56
|
+
async function bundleAgent(agent, opts) {
|
|
57
|
+
const aaiDir = path.join(agent.dir, ".aai");
|
|
58
|
+
const buildDir = path.join(aaiDir, "build");
|
|
59
|
+
const clientDir = path.join(aaiDir, "client");
|
|
60
|
+
try {
|
|
61
|
+
await build({
|
|
62
|
+
root: agent.dir,
|
|
63
|
+
logLevel: "warn",
|
|
64
|
+
build: {
|
|
65
|
+
ssr: path.join(agent.dir, "agent.ts"),
|
|
66
|
+
outDir: buildDir,
|
|
67
|
+
emptyOutDir: true,
|
|
68
|
+
rollupOptions: { output: { entryFileNames: "worker.js" } }
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw new BundleError(errorMessage(err), { cause: err });
|
|
73
|
+
}
|
|
74
|
+
if (!(opts?.skipClient ?? !agent.clientEntry)) try {
|
|
75
|
+
await build({
|
|
76
|
+
root: agent.dir,
|
|
77
|
+
base: "./",
|
|
78
|
+
logLevel: "warn",
|
|
79
|
+
build: {
|
|
80
|
+
outDir: clientDir,
|
|
81
|
+
emptyOutDir: true
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
throw new BundleError(errorMessage(err), { cause: err });
|
|
86
|
+
}
|
|
87
|
+
const worker = await fs.readFile(path.join(buildDir, "worker.js"), "utf-8");
|
|
88
|
+
const clientFiles = await readDirFiles(clientDir);
|
|
89
|
+
return {
|
|
90
|
+
slug: agent.slug,
|
|
91
|
+
worker,
|
|
92
|
+
clientFiles,
|
|
93
|
+
clientDir,
|
|
94
|
+
workerBytes: Buffer.byteLength(worker)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function buildAgentBundle(cwd) {
|
|
98
|
+
const { loadAgent } = await import("./_discover-a8yIuqEp.mjs").then((n) => n.t);
|
|
99
|
+
const { log } = await import("./_ui-DWGXImbO.mjs");
|
|
100
|
+
const agent = await loadAgent(cwd);
|
|
101
|
+
if (!agent) throw new Error("No agent found — run `aai init` first");
|
|
102
|
+
log.step(`Bundling ${agent.slug}`);
|
|
103
|
+
let bundle;
|
|
104
|
+
try {
|
|
105
|
+
bundle = await bundleAgent(agent);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err instanceof BundleError) throw new Error(`Build failed: ${err.message}`, { cause: err });
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
return bundle;
|
|
111
|
+
}
|
|
112
|
+
async function runBuildCommand(cwd) {
|
|
113
|
+
const { log } = await import("./_ui-DWGXImbO.mjs");
|
|
114
|
+
await buildAgentBundle(cwd);
|
|
115
|
+
log.success("Build complete");
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { buildAgentBundle, runBuildCommand };
|
|
@@ -2,13 +2,24 @@
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import fs
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
6
|
import { consola } from "consola";
|
|
7
7
|
import { humanId } from "human-id";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { createInterface } from "node:readline";
|
|
10
|
+
//#region \0rolldown/runtime.js
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __exportAll = (all, no_symbols) => {
|
|
13
|
+
let target = {};
|
|
14
|
+
for (var name in all) __defProp(target, name, {
|
|
15
|
+
get: all[name],
|
|
16
|
+
enumerable: true
|
|
17
|
+
});
|
|
18
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
19
|
+
return target;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
10
22
|
//#region _prompts.ts
|
|
11
|
-
const consola$1 = consola.create({ formatOptions: { date: false } });
|
|
12
23
|
/**
|
|
13
24
|
* Prompt the user for a password (masked input).
|
|
14
25
|
* Returns the entered string.
|
|
@@ -47,20 +58,23 @@ function readMasked(stdin) {
|
|
|
47
58
|
stdin.on("data", onData);
|
|
48
59
|
});
|
|
49
60
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Prompt the user for text input with a default value.
|
|
52
|
-
* Returns the entered string, or the default if empty.
|
|
53
|
-
*/
|
|
54
|
-
async function askText(message, defaultValue) {
|
|
55
|
-
return await consola$1.prompt(message, {
|
|
56
|
-
type: "text",
|
|
57
|
-
placeholder: defaultValue,
|
|
58
|
-
initial: defaultValue,
|
|
59
|
-
cancel: "reject"
|
|
60
|
-
}) || defaultValue;
|
|
61
|
-
}
|
|
62
61
|
//#endregion
|
|
63
62
|
//#region _discover.ts
|
|
63
|
+
var _discover_exports = /* @__PURE__ */ __exportAll({
|
|
64
|
+
DEFAULT_DEV_SERVER: () => DEFAULT_DEV_SERVER,
|
|
65
|
+
DEFAULT_SERVER: () => DEFAULT_SERVER,
|
|
66
|
+
ensureApiKeyInEnv: () => ensureApiKeyInEnv,
|
|
67
|
+
fileExists: () => fileExists,
|
|
68
|
+
generateSlug: () => generateSlug,
|
|
69
|
+
getApiKey: () => getApiKey,
|
|
70
|
+
getServerInfo: () => getServerInfo,
|
|
71
|
+
isDevMode: () => isDevMode,
|
|
72
|
+
loadAgent: () => loadAgent,
|
|
73
|
+
readProjectConfig: () => readProjectConfig,
|
|
74
|
+
resolveCwd: () => resolveCwd,
|
|
75
|
+
resolveServerUrl: () => resolveServerUrl,
|
|
76
|
+
writeProjectConfig: () => writeProjectConfig
|
|
77
|
+
});
|
|
64
78
|
const AuthConfigSchema = z.object({ assemblyai_api_key: z.string().optional() });
|
|
65
79
|
const ProjectConfigSchema = z.object({
|
|
66
80
|
slug: z.string(),
|
|
@@ -84,15 +98,16 @@ const CONFIG_DIR = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".",
|
|
|
84
98
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
85
99
|
async function readAuthConfig() {
|
|
86
100
|
try {
|
|
87
|
-
return AuthConfigSchema.parse(JSON.parse(await fs
|
|
88
|
-
} catch {
|
|
101
|
+
return AuthConfigSchema.parse(JSON.parse(await fs.readFile(CONFIG_FILE, "utf-8")));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
consola.debug(`Failed to read auth config from ${CONFIG_FILE}:`, error);
|
|
89
104
|
return {};
|
|
90
105
|
}
|
|
91
106
|
}
|
|
92
107
|
async function writeAuthConfig(config) {
|
|
93
|
-
await fs
|
|
94
|
-
await fs
|
|
95
|
-
if (process.platform !== "win32") await fs
|
|
108
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
109
|
+
await fs.writeFile(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`);
|
|
110
|
+
if (process.platform !== "win32") await fs.chmod(CONFIG_FILE, 384);
|
|
96
111
|
}
|
|
97
112
|
/**
|
|
98
113
|
* Retrieves the AssemblyAI API key from `process.env`, `~/.config/aai/config.json`,
|
|
@@ -105,8 +120,9 @@ async function getApiKey() {
|
|
|
105
120
|
if (process.env.ASSEMBLYAI_API_KEY) return process.env.ASSEMBLYAI_API_KEY;
|
|
106
121
|
const config = await readAuthConfig();
|
|
107
122
|
if (config.assemblyai_api_key) return config.assemblyai_api_key;
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
const { log } = await import("./_ui-DWGXImbO.mjs");
|
|
124
|
+
log.info("Get your API key at https://www.assemblyai.com/dashboard/signup");
|
|
125
|
+
log.info("Or set the ASSEMBLYAI_API_KEY environment variable to skip this prompt.");
|
|
110
126
|
let key;
|
|
111
127
|
while (!key) key = await askPassword("ASSEMBLYAI_API_KEY");
|
|
112
128
|
config.assemblyai_api_key = key;
|
|
@@ -128,8 +144,9 @@ async function ensureApiKeyInEnv() {
|
|
|
128
144
|
*/
|
|
129
145
|
async function readProjectConfig(agentDir) {
|
|
130
146
|
try {
|
|
131
|
-
return ProjectConfigSchema.parse(JSON.parse(await fs
|
|
132
|
-
} catch {
|
|
147
|
+
return ProjectConfigSchema.parse(JSON.parse(await fs.readFile(path.join(agentDir, ".aai", "project.json"), "utf-8")));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
consola.debug(`Failed to read project config from ${path.join(agentDir, ".aai", "project.json")}:`, error);
|
|
133
150
|
return null;
|
|
134
151
|
}
|
|
135
152
|
}
|
|
@@ -138,16 +155,16 @@ async function readProjectConfig(agentDir) {
|
|
|
138
155
|
*/
|
|
139
156
|
async function writeProjectConfig(agentDir, data) {
|
|
140
157
|
const aaiDir = path.join(agentDir, ".aai");
|
|
141
|
-
await fs
|
|
142
|
-
await fs
|
|
158
|
+
await fs.mkdir(aaiDir, { recursive: true });
|
|
159
|
+
await fs.writeFile(path.join(aaiDir, "project.json"), `${JSON.stringify(data, null, 2)}\n`);
|
|
143
160
|
}
|
|
144
161
|
/**
|
|
145
162
|
* Read project config (throws if missing), resolve API key and server URL.
|
|
146
|
-
* Shared by secret
|
|
163
|
+
* Shared by secret commands.
|
|
147
164
|
*/
|
|
148
165
|
async function getServerInfo(cwd, explicitServer, explicitApiKey) {
|
|
149
166
|
const config = await readProjectConfig(cwd);
|
|
150
|
-
if (!config) throw new Error("No .aai/project.json found —
|
|
167
|
+
if (!config) throw new Error("No .aai/project.json found — run `aai deploy` first");
|
|
151
168
|
const apiKey = explicitApiKey ?? await getApiKey();
|
|
152
169
|
return {
|
|
153
170
|
serverUrl: resolveServerUrl(explicitServer, config.serverUrl),
|
|
@@ -155,6 +172,8 @@ async function getServerInfo(cwd, explicitServer, explicitApiKey) {
|
|
|
155
172
|
apiKey
|
|
156
173
|
};
|
|
157
174
|
}
|
|
175
|
+
/** Default production server URL for agent deployments. */
|
|
176
|
+
const DEFAULT_SERVER = "https://aai-agent.fly.dev";
|
|
158
177
|
/** Default local dev server URL. */
|
|
159
178
|
const DEFAULT_DEV_SERVER = "http://localhost:8787";
|
|
160
179
|
/** Check if the CLI is running from the monorepo (dev mode). */
|
|
@@ -172,9 +191,10 @@ function resolveServerUrl(explicit, configUrl) {
|
|
|
172
191
|
}
|
|
173
192
|
async function fileExists(p) {
|
|
174
193
|
try {
|
|
175
|
-
await fs
|
|
194
|
+
await fs.access(p);
|
|
176
195
|
return true;
|
|
177
|
-
} catch {
|
|
196
|
+
} catch (error) {
|
|
197
|
+
consola.debug(`File access check failed for ${p}:`, error);
|
|
178
198
|
return false;
|
|
179
199
|
}
|
|
180
200
|
}
|
|
@@ -197,4 +217,4 @@ async function loadAgent(dir) {
|
|
|
197
217
|
};
|
|
198
218
|
}
|
|
199
219
|
//#endregion
|
|
200
|
-
export {
|
|
220
|
+
export { getApiKey as a, readProjectConfig as c, writeProjectConfig as d, askPassword as f, generateSlug as i, resolveCwd as l, ensureApiKeyInEnv as n, getServerInfo as o, fileExists as r, isDevMode as s, _discover_exports as t, resolveServerUrl as u };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as downloadAndMergeTemplate } from "./_templates-CtZBILce.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
//#region _init.ts
|
|
6
|
+
async function runInit(opts) {
|
|
7
|
+
const { targetDir, template } = opts;
|
|
8
|
+
await downloadAndMergeTemplate(template, targetDir);
|
|
9
|
+
try {
|
|
10
|
+
await fs.copyFile(path.join(targetDir, ".env.example"), path.join(targetDir, ".env"));
|
|
11
|
+
} catch {}
|
|
12
|
+
const readmePath = path.join(targetDir, "README.md");
|
|
13
|
+
const readme = `# ${path.basename(path.resolve(targetDir))}
|
|
14
|
+
|
|
15
|
+
A voice agent built with [aai](https://github.com/anthropics/aai).
|
|
16
|
+
|
|
17
|
+
## Getting started
|
|
18
|
+
|
|
19
|
+
\`\`\`sh
|
|
20
|
+
npm install # Install dependencies
|
|
21
|
+
aai dev # Run locally (opens browser)
|
|
22
|
+
aai deploy # Deploy to production
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
## Secrets
|
|
26
|
+
|
|
27
|
+
Access secrets in your agent via \`ctx.env.MY_KEY\`.
|
|
28
|
+
|
|
29
|
+
**Local development** — add secrets to \`.env\` (auto-loaded by \`aai dev\`):
|
|
30
|
+
|
|
31
|
+
\`\`\`sh
|
|
32
|
+
ALPHA_VANTAGE_KEY=sk-abc123
|
|
33
|
+
MY_API_KEY=secret-value
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
**Production** — set secrets on the server:
|
|
37
|
+
|
|
38
|
+
\`\`\`sh
|
|
39
|
+
aai secret put MY_KEY # Set a secret (prompts for value)
|
|
40
|
+
aai secret list # List secret names
|
|
41
|
+
aai secret delete MY_KEY # Remove a secret
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
## Learn more
|
|
45
|
+
|
|
46
|
+
See \`CLAUDE.md\` for the full agent API reference.
|
|
47
|
+
`;
|
|
48
|
+
try {
|
|
49
|
+
await fs.writeFile(readmePath, readme, { flag: "wx" });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
|
|
52
|
+
}
|
|
53
|
+
return targetDir;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { runInit };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { s as isDevMode } from "./_discover-a8yIuqEp.mjs";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import { downloadTemplate } from "giget";
|
|
8
|
+
//#region _templates.ts
|
|
9
|
+
const GIGET_SOURCE = "github:alexkroman/agent/packages/aai-templates";
|
|
10
|
+
const GIGET_REF = process.env.AAI_TEMPLATES_REF ?? "main";
|
|
11
|
+
/** Resolve the local aai-templates package directory (dev mode only). */
|
|
12
|
+
function resolveLocalTemplatesDir() {
|
|
13
|
+
const cliDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const fromSrc = path.resolve(cliDir, "../aai-templates");
|
|
15
|
+
const fromDist = path.resolve(cliDir, "../../aai-templates");
|
|
16
|
+
if (existsSync(fromSrc)) return fromSrc;
|
|
17
|
+
if (existsSync(fromDist)) return fromDist;
|
|
18
|
+
throw new Error("Cannot find local aai-templates package");
|
|
19
|
+
}
|
|
20
|
+
/** Resolve the templates directory — local in dev, giget download in prod. */
|
|
21
|
+
async function resolveTemplatesDir() {
|
|
22
|
+
if (isDevMode()) return resolveLocalTemplatesDir();
|
|
23
|
+
const { dir } = await downloadTemplate(`${GIGET_SOURCE}#${GIGET_REF}`, { force: true });
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
/** List available templates with descriptions. */
|
|
27
|
+
async function listTemplates() {
|
|
28
|
+
const root = await resolveTemplatesDir();
|
|
29
|
+
const dir = path.join(root, "templates");
|
|
30
|
+
const names = (await fs.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
31
|
+
let descriptions = {};
|
|
32
|
+
try {
|
|
33
|
+
descriptions = JSON.parse(await fs.readFile(path.join(root, "templates.json"), "utf-8"));
|
|
34
|
+
} catch {}
|
|
35
|
+
return names.map((name) => ({
|
|
36
|
+
name,
|
|
37
|
+
description: descriptions[name] ?? ""
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Download a template into targetDir, merging scaffold files underneath.
|
|
42
|
+
*/
|
|
43
|
+
async function downloadAndMergeTemplate(template, targetDir) {
|
|
44
|
+
const root = await resolveTemplatesDir();
|
|
45
|
+
const templatesDir = path.join(root, "templates");
|
|
46
|
+
const names = (await fs.readdir(templatesDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
47
|
+
if (!names.includes(template)) throw new Error(`Unknown template "${template}". Available templates: ${names.join(", ")}`);
|
|
48
|
+
await fs.cp(path.join(templatesDir, template), targetDir, {
|
|
49
|
+
recursive: true,
|
|
50
|
+
force: true
|
|
51
|
+
});
|
|
52
|
+
const scaffoldDir = path.join(root, "scaffold");
|
|
53
|
+
if (existsSync(scaffoldDir)) {
|
|
54
|
+
const entries = await fs.readdir(scaffoldDir, {
|
|
55
|
+
recursive: true,
|
|
56
|
+
withFileTypes: true
|
|
57
|
+
});
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (!entry.isFile()) continue;
|
|
60
|
+
const rel = path.relative(scaffoldDir, path.join(entry.parentPath, entry.name));
|
|
61
|
+
const destPath = path.join(targetDir, rel);
|
|
62
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
63
|
+
try {
|
|
64
|
+
await fs.copyFile(path.join(scaffoldDir, rel), destPath, fs.constants.COPYFILE_EXCL);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { listTemplates as n, downloadAndMergeTemplate as t };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { colorize } from "consola/utils";
|
|
4
|
+
//#region _ui.ts
|
|
5
|
+
/**
|
|
6
|
+
* Unified CLI output using @clack/prompts style (◐ ◇ │).
|
|
7
|
+
*
|
|
8
|
+
* All commands should use these helpers instead of consola directly
|
|
9
|
+
* so the output is visually consistent.
|
|
10
|
+
*/
|
|
11
|
+
const log = {
|
|
12
|
+
step: (msg) => p.log.step(msg),
|
|
13
|
+
success: (msg) => p.log.success(msg),
|
|
14
|
+
info: (msg) => p.log.info(msg),
|
|
15
|
+
warn: (msg) => p.log.warn(msg),
|
|
16
|
+
error: (msg) => p.log.error(msg),
|
|
17
|
+
message: (msg) => p.log.message(msg)
|
|
18
|
+
};
|
|
19
|
+
/** Format a URL for display. */
|
|
20
|
+
function fmtUrl(url) {
|
|
21
|
+
return colorize("cyanBright", url);
|
|
22
|
+
}
|
|
23
|
+
/** Parse and validate a port string. Returns the numeric port or throws. */
|
|
24
|
+
function parsePort(raw) {
|
|
25
|
+
const port = Number.parseInt(raw, 10);
|
|
26
|
+
if (Number.isNaN(port) || port < 0 || port > 65535) throw new Error(`Invalid port: ${raw}. Must be a number between 0 and 65535.`);
|
|
27
|
+
return port;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { fmtUrl, log, parsePort };
|