@cargo-ai/cdk 1.0.6 → 1.0.8
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/README.md +4 -44
- package/build/src/cli/auth.d.ts.map +1 -1
- package/build/src/cli/auth.js +16 -12
- package/build/src/cli/commands/deploy.d.ts.map +1 -1
- package/build/src/cli/commands/deploy.js +81 -107
- package/build/src/cli/commands/from.d.ts +65 -0
- package/build/src/cli/commands/from.d.ts.map +1 -0
- package/build/src/cli/commands/from.js +285 -0
- package/build/src/cli/commands/init.d.ts +1 -1
- package/build/src/cli/commands/init.d.ts.map +1 -1
- package/build/src/cli/commands/init.js +44 -20
- package/build/src/cli/commands/inputTypes.d.ts.map +1 -1
- package/build/src/cli/commands/inputTypes.js +77 -6
- package/build/src/cli/commands/types.d.ts.map +1 -1
- package/build/src/cli/commands/types.js +87 -9
- package/build/src/cli/io.js +2 -2
- package/build/src/cli/version.d.ts.map +1 -1
- package/build/src/cli/version.js +10 -6
- package/build/src/deploy/apply.d.ts.map +1 -1
- package/build/src/deploy/apply.js +37 -30
- package/build/src/deploy/compile.d.ts.map +1 -1
- package/build/src/deploy/compile.js +7 -5
- package/build/src/deploy/destroy.d.ts.map +1 -1
- package/build/src/deploy/destroy.js +24 -14
- package/build/src/deploy/executors.live.d.ts.map +1 -1
- package/build/src/deploy/executors.live.js +202 -173
- package/build/src/deploy/import.js +2 -2
- package/build/src/deploy/index.js +1 -1
- package/build/src/deploy/lock.d.ts.map +1 -1
- package/build/src/deploy/lock.js +13 -10
- package/build/src/deploy/readers.live.d.ts.map +1 -1
- package/build/src/deploy/readers.live.js +27 -15
- package/build/src/deploy/refresh.d.ts.map +1 -1
- package/build/src/deploy/refresh.js +20 -18
- package/build/src/index.d.ts +4 -4
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +2 -2
- package/build/src/refs.d.ts.map +1 -1
- package/build/src/refs.js +5 -1
- package/build/src/resources/actions.d.ts +1 -1
- package/build/src/resources/actions.d.ts.map +1 -1
- package/build/src/resources/actions.js +3 -2
- package/build/src/resources/agent.d.ts +23 -0
- package/build/src/resources/agent.d.ts.map +1 -1
- package/build/src/resources/agent.js +17 -0
- package/build/src/resources/capacity.d.ts +11 -0
- package/build/src/resources/capacity.d.ts.map +1 -1
- package/build/src/resources/capacity.js +81 -0
- package/build/src/resources/model.d.ts +11 -3
- package/build/src/resources/model.d.ts.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// `cdk init <dir> --from <source>` — scaffold from a GitHub repo instead of a
|
|
2
|
+
// bundled template. The source names a repo or a folder inside one
|
|
3
|
+
// (`owner/repo`, `owner/repo/folder`, `owner/repo/folder@ref`, or a
|
|
4
|
+
// github.com URL). The repo is fetched with a shallow `git clone`, so private
|
|
5
|
+
// sources work through the user's existing git auth.
|
|
6
|
+
//
|
|
7
|
+
// The manifest is OPTIONAL — any repo/folder scaffolds without it. A repo of
|
|
8
|
+
// composable folders (e.g. cargo-cookbooks) can ship a `cargo.scaffold.json`
|
|
9
|
+
// at its root to declare cross-folder dependencies:
|
|
10
|
+
//
|
|
11
|
+
// {
|
|
12
|
+
// "shared": ["package.json", "tsconfig.json"],
|
|
13
|
+
// "folders": { "signal-based-tam": { "requires": ["base-gtm"] } }
|
|
14
|
+
// }
|
|
15
|
+
//
|
|
16
|
+
// When the requested folder is declared there, the scaffold keeps the repo's
|
|
17
|
+
// folder layout and also copies its `requires` folders (transitively) plus the
|
|
18
|
+
// `shared` root files — so cross-folder imports like
|
|
19
|
+
// `../base-gtm/models/accounts` keep resolving. Any other source copies the
|
|
20
|
+
// folder's contents flat into the target directory.
|
|
21
|
+
import { execFile } from "node:child_process";
|
|
22
|
+
import { promises as fs } from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
25
|
+
import { promisify } from "node:util";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { copyDirectory } from "./templates.js";
|
|
28
|
+
const execFileAsync = promisify(execFile);
|
|
29
|
+
const SEGMENT_PATTERN = /^[\w][\w.-]*$/;
|
|
30
|
+
/** Reject absolute paths, `..`, `.`, and empty segments in repo-relative paths. */
|
|
31
|
+
export function assertSafeRepoRelativePath(repoRelativePath, label) {
|
|
32
|
+
if (repoRelativePath === "") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (isAbsolute(repoRelativePath)) {
|
|
36
|
+
throw new Error(`Invalid ${label}: absolute paths are not allowed.`);
|
|
37
|
+
}
|
|
38
|
+
const segments = repoRelativePath.split(/[/\\]/);
|
|
39
|
+
for (const segment of segments) {
|
|
40
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
41
|
+
throw new Error(`Invalid ${label}: "${repoRelativePath}" contains an unsafe path segment.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Join `relativePath` under `baseDir` and ensure the result cannot escape it. */
|
|
46
|
+
export function resolveWithin(baseDir, relativePath) {
|
|
47
|
+
const resolvedBase = resolve(baseDir);
|
|
48
|
+
const resolved = resolve(resolvedBase, relativePath);
|
|
49
|
+
const rel = relative(resolvedBase, resolved);
|
|
50
|
+
if (rel === "" || rel === ".") {
|
|
51
|
+
return resolvedBase;
|
|
52
|
+
}
|
|
53
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
54
|
+
throw new Error(`Path "${relativePath}" escapes "${baseDir}".`);
|
|
55
|
+
}
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
function assertSafeSubPath(subPath) {
|
|
59
|
+
try {
|
|
60
|
+
assertSafeRepoRelativePath(subPath, "source folder");
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function validateManifestPaths(manifest) {
|
|
68
|
+
for (const file of manifest.shared) {
|
|
69
|
+
assertSafeRepoRelativePath(file, `${MANIFEST_FILENAME} shared entry`);
|
|
70
|
+
}
|
|
71
|
+
for (const [folder, entry] of Object.entries(manifest.folders)) {
|
|
72
|
+
assertSafeRepoRelativePath(folder, `${MANIFEST_FILENAME} folder key`);
|
|
73
|
+
for (const required of entry.requires) {
|
|
74
|
+
assertSafeRepoRelativePath(required, `${MANIFEST_FILENAME} requires entry for "${folder}"`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse a `--from` source. Accepts `owner/repo[/sub/path][@ref]`, the same
|
|
80
|
+
* with a `github.com` / `git@github.com:` prefix, and GitHub tree URLs
|
|
81
|
+
* (`github.com/owner/repo/tree/<ref>/<sub/path>`). Returns `undefined` when
|
|
82
|
+
* the source doesn't name a GitHub repo.
|
|
83
|
+
*/
|
|
84
|
+
export function parseFromRef(raw) {
|
|
85
|
+
const trimmed = raw.trim();
|
|
86
|
+
const isGitHubUrl = /^https?:\/\/(www\.)?github\.com\//.test(trimmed) ||
|
|
87
|
+
/^git@github\.com:/.test(trimmed);
|
|
88
|
+
const stripped = trimmed
|
|
89
|
+
.replace(/^git@github\.com:/, "")
|
|
90
|
+
.replace(/^https?:\/\/(www\.)?github\.com\//, "")
|
|
91
|
+
.replace(/\/+$/, "");
|
|
92
|
+
if (stripped.includes("://") || stripped.startsWith("git@")) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const atIndex = stripped.lastIndexOf("@");
|
|
96
|
+
const path = atIndex === -1 ? stripped : stripped.slice(0, atIndex);
|
|
97
|
+
const refSuffix = atIndex === -1 ? "" : stripped.slice(atIndex + 1);
|
|
98
|
+
if (atIndex !== -1 && refSuffix === "")
|
|
99
|
+
return undefined;
|
|
100
|
+
const segments = path.split("/").filter((segment) => segment !== "");
|
|
101
|
+
const [owner, repoRaw] = segments;
|
|
102
|
+
if (owner === undefined || repoRaw === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
const repo = repoRaw.replace(/\.git$/, "");
|
|
105
|
+
if (SEGMENT_PATTERN.test(owner) === false ||
|
|
106
|
+
SEGMENT_PATTERN.test(repo) === false) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
// A tree URL pins its ref in the path: owner/repo/tree/<ref>/<sub/path>.
|
|
110
|
+
// Only apply for full GitHub URLs — otherwise "tree" may be a real folder
|
|
111
|
+
// (e.g. owner/repo/tree/my-feature).
|
|
112
|
+
if (isGitHubUrl && segments[2] === "tree") {
|
|
113
|
+
const treeRef = segments[3];
|
|
114
|
+
if (treeRef === undefined || refSuffix !== "")
|
|
115
|
+
return undefined;
|
|
116
|
+
const subPath = segments.slice(4).join("/");
|
|
117
|
+
if (assertSafeSubPath(subPath) === false)
|
|
118
|
+
return undefined;
|
|
119
|
+
return {
|
|
120
|
+
owner,
|
|
121
|
+
repo,
|
|
122
|
+
subPath,
|
|
123
|
+
gitRef: treeRef,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const subPath = segments.slice(2).join("/");
|
|
127
|
+
if (assertSafeSubPath(subPath) === false)
|
|
128
|
+
return undefined;
|
|
129
|
+
return {
|
|
130
|
+
owner,
|
|
131
|
+
repo,
|
|
132
|
+
subPath,
|
|
133
|
+
gitRef: refSuffix,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const manifestSchema = z.object({
|
|
137
|
+
shared: z.array(z.string()).default([]),
|
|
138
|
+
folders: z
|
|
139
|
+
.record(z.string(), z.object({ requires: z.array(z.string()).default([]) }))
|
|
140
|
+
.default({}),
|
|
141
|
+
});
|
|
142
|
+
export const MANIFEST_FILENAME = "cargo.scaffold.json";
|
|
143
|
+
// Read the manifest file's raw contents; undefined when it doesn't exist
|
|
144
|
+
// (ENOENT), rethrowing any other read error.
|
|
145
|
+
async function readManifestFile(manifestPath) {
|
|
146
|
+
try {
|
|
147
|
+
return await fs.readFile(manifestPath, "utf-8");
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error !== null &&
|
|
151
|
+
typeof error === "object" &&
|
|
152
|
+
"code" in error &&
|
|
153
|
+
error.code === "ENOENT") {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
157
|
+
throw new Error(`Could not read ${MANIFEST_FILENAME} in the source repo: ${message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Parse the manifest text, turning a JSON syntax error into a clear message.
|
|
161
|
+
function parseManifestJson(raw) {
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(raw);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
+
throw new Error(`Invalid ${MANIFEST_FILENAME} in the source repo: ${message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Read and validate `cargo.scaffold.json` at the repo root, if present. */
|
|
171
|
+
export async function readManifest(repoDir) {
|
|
172
|
+
const manifestPath = join(repoDir, MANIFEST_FILENAME);
|
|
173
|
+
const raw = await readManifestFile(manifestPath);
|
|
174
|
+
if (raw === undefined)
|
|
175
|
+
return undefined;
|
|
176
|
+
const json = parseManifestJson(raw);
|
|
177
|
+
const parsed = manifestSchema.safeParse(json);
|
|
178
|
+
if (parsed.success === false) {
|
|
179
|
+
throw new Error(`Invalid ${MANIFEST_FILENAME} in the source repo: ${z.prettifyError(parsed.error)}`);
|
|
180
|
+
}
|
|
181
|
+
validateManifestPaths(parsed.data);
|
|
182
|
+
return parsed.data;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Decide what to copy. A manifest-declared folder keeps the repo layout and
|
|
186
|
+
* brings its transitive `requires` plus the shared root files; anything else
|
|
187
|
+
* is a flat copy of the requested folder (or the whole repo).
|
|
188
|
+
*/
|
|
189
|
+
export function planCopies(manifest, subPath) {
|
|
190
|
+
const declared = manifest === undefined ? undefined : manifest.folders[subPath];
|
|
191
|
+
if (manifest === undefined || declared === undefined || subPath === "") {
|
|
192
|
+
return { folders: [subPath], files: [], flatten: true };
|
|
193
|
+
}
|
|
194
|
+
const folders = [];
|
|
195
|
+
const queue = [subPath];
|
|
196
|
+
while (queue.length > 0) {
|
|
197
|
+
const current = queue.shift();
|
|
198
|
+
if (current === undefined || folders.includes(current))
|
|
199
|
+
continue;
|
|
200
|
+
folders.push(current);
|
|
201
|
+
const entry = manifest.folders[current];
|
|
202
|
+
if (entry !== undefined)
|
|
203
|
+
queue.push(...entry.requires);
|
|
204
|
+
}
|
|
205
|
+
return { folders, files: manifest.shared, flatten: false };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Shallow-clone the repo into a temp dir. Returns the dir and its cleanup.
|
|
209
|
+
* `remoteUrl` overrides the GitHub URL (tests clone from a local repo).
|
|
210
|
+
*/
|
|
211
|
+
export async function cloneGitHubRepo(ref, remoteUrl) {
|
|
212
|
+
const dir = await fs.mkdtemp(join(tmpdir(), "cargo-cdk-from-"));
|
|
213
|
+
const url = remoteUrl === undefined
|
|
214
|
+
? `https://github.com/${ref.owner}/${ref.repo}.git`
|
|
215
|
+
: remoteUrl;
|
|
216
|
+
const branchArgs = ref.gitRef === "" ? [] : ["--branch", ref.gitRef];
|
|
217
|
+
try {
|
|
218
|
+
await execFileAsync("git", [
|
|
219
|
+
"clone",
|
|
220
|
+
"--depth",
|
|
221
|
+
"1",
|
|
222
|
+
"--quiet",
|
|
223
|
+
...branchArgs,
|
|
224
|
+
url,
|
|
225
|
+
dir,
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
230
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
231
|
+
throw new Error(`Could not clone ${url}${ref.gitRef === "" ? "" : ` (ref ${ref.gitRef})`}. Is git installed and the repo accessible?\n${message}`);
|
|
232
|
+
}
|
|
233
|
+
// Drop the git metadata so a whole-repo copy never carries it over.
|
|
234
|
+
await fs.rm(join(dir, ".git"), { recursive: true, force: true });
|
|
235
|
+
return {
|
|
236
|
+
dir,
|
|
237
|
+
cleanup: () => fs.rm(dir, { recursive: true, force: true }),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Fetch the source and copy the planned folders/files into `targetDir`,
|
|
242
|
+
* substituting `__APP_NAME__`. Returns the copied repo-relative paths.
|
|
243
|
+
*/
|
|
244
|
+
export async function scaffoldFromGitHub(payload) {
|
|
245
|
+
const { ref, targetDir, appName, remoteUrl } = payload;
|
|
246
|
+
const rename = [{ from: "__APP_NAME__", to: appName }];
|
|
247
|
+
const { dir, cleanup } = await cloneGitHubRepo(ref, remoteUrl);
|
|
248
|
+
try {
|
|
249
|
+
if (ref.subPath !== "") {
|
|
250
|
+
assertSafeRepoRelativePath(ref.subPath, "source folder");
|
|
251
|
+
const subPathSrc = resolveWithin(dir, ref.subPath);
|
|
252
|
+
const stat = await fs.stat(subPathSrc).catch(() => undefined);
|
|
253
|
+
if (stat === undefined || stat.isDirectory() === false) {
|
|
254
|
+
throw new Error(`"${ref.subPath}" is not a folder in ${ref.owner}/${ref.repo}.`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const manifest = await readManifest(dir);
|
|
258
|
+
const plan = planCopies(manifest, ref.subPath);
|
|
259
|
+
for (const folder of plan.folders) {
|
|
260
|
+
const src = resolveWithin(dir, folder);
|
|
261
|
+
const dest = plan.flatten ? targetDir : resolveWithin(targetDir, folder);
|
|
262
|
+
const stat = await fs.stat(src).catch(() => undefined);
|
|
263
|
+
if (stat === undefined || stat.isDirectory() === false) {
|
|
264
|
+
throw new Error(`Manifest folder "${folder}" is missing from ${ref.owner}/${ref.repo}.`);
|
|
265
|
+
}
|
|
266
|
+
await copyDirectory(src, dest, rename);
|
|
267
|
+
}
|
|
268
|
+
for (const file of plan.files) {
|
|
269
|
+
const src = resolveWithin(dir, file);
|
|
270
|
+
const stat = await fs.stat(src).catch(() => undefined);
|
|
271
|
+
if (stat === undefined || stat.isFile() === false) {
|
|
272
|
+
throw new Error(`Manifest shared file "${file}" is missing from ${ref.owner}/${ref.repo}.`);
|
|
273
|
+
}
|
|
274
|
+
const raw = await fs.readFile(src, "utf-8");
|
|
275
|
+
const replaced = rename.reduce((current, { from, to }) => current.split(from).join(to), raw);
|
|
276
|
+
const dest = resolveWithin(targetDir, file);
|
|
277
|
+
await fs.mkdir(dirname(dest), { recursive: true });
|
|
278
|
+
await fs.writeFile(dest, replaced, "utf-8");
|
|
279
|
+
}
|
|
280
|
+
return [...plan.folders, ...plan.files].filter((path) => path !== "");
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
await cleanup();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,WAAW,CAAC;AAmBjD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAqGzD"}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Option } from "commander";
|
|
4
5
|
import { ExitCodes, failWith, info, success } from "../io.js";
|
|
6
|
+
import { parseFromRef, scaffoldFromGitHub } from "./from.js";
|
|
5
7
|
import { copyDirectory, listTemplates } from "./templates.js";
|
|
6
|
-
// `cdk init <directory> [--template <slug>]` — scaffold a
|
|
7
|
-
// copying one of `@cargo-ai/cdk`'s bundled templates
|
|
8
|
-
//
|
|
8
|
+
// `cdk init <directory> [--template <slug>] [--from <source>]` — scaffold a
|
|
9
|
+
// starter CDK project by copying one of `@cargo-ai/cdk`'s bundled templates
|
|
10
|
+
// (see `--list-templates`) or fetching a GitHub source — e.g. a cookbook:
|
|
11
|
+
// `cdk init my-tam --from getcargohq/cargo-cookbooks/signal-based-tam` — substituting
|
|
12
|
+
// `__APP_NAME__`. Cookbook sources pull their required sibling folders via the
|
|
13
|
+
// repo's `cargo.scaffold.json` (see `./from.ts`).
|
|
9
14
|
const TEMPLATE_DESCRIPTIONS = {
|
|
10
15
|
blank: "Minimal starter — one connector + model + a workflow-backed tool. Good starting point.",
|
|
11
16
|
full: "The full example — every resource type wired into a GTM growth workspace: connectors, models, plays, agents, an MCP server, context, tools, a worker, and a hosted app.",
|
|
@@ -14,7 +19,10 @@ export function registerInitCommand(parent) {
|
|
|
14
19
|
parent
|
|
15
20
|
.command("init <directory>")
|
|
16
21
|
.description("Scaffold a starter Cargo CDK project locally from a template (ready to plan/deploy).")
|
|
17
|
-
.
|
|
22
|
+
.addOption(new Option("--template <slug>", "Template slug (default: blank)")
|
|
23
|
+
.default("blank")
|
|
24
|
+
.conflicts("from"))
|
|
25
|
+
.option("--from <source>", "GitHub source to scaffold from instead of a template — owner/repo[/folder][@ref] or a github.com URL. Cookbook folders also pull their required siblings (repo's cargo.scaffold.json).")
|
|
18
26
|
.option("--name <name>", "Project name written into package.json")
|
|
19
27
|
.option("--list-templates", "Print available templates and exit")
|
|
20
28
|
.option("--force", "Write into a non-empty directory")
|
|
@@ -23,16 +31,12 @@ export function registerInitCommand(parent) {
|
|
|
23
31
|
if (opts.listTemplates === true) {
|
|
24
32
|
const slugs = await listTemplates(templatesRoot);
|
|
25
33
|
for (const slug of slugs) {
|
|
26
|
-
const
|
|
34
|
+
const described = TEMPLATE_DESCRIPTIONS[slug];
|
|
35
|
+
const desc = described === undefined ? "" : described;
|
|
27
36
|
info(`${slug}${desc.length > 0 ? ` — ${desc}` : ""}`);
|
|
28
37
|
}
|
|
29
38
|
return;
|
|
30
39
|
}
|
|
31
|
-
const templateDir = join(templatesRoot, opts.template);
|
|
32
|
-
if (!existsSync(templateDir)) {
|
|
33
|
-
const slugs = await listTemplates(templatesRoot);
|
|
34
|
-
failWith(`Unknown template "${opts.template}". Available: ${slugs.join(", ")}`, { code: ExitCodes.GenericError });
|
|
35
|
-
}
|
|
36
40
|
const targetDir = resolve(process.cwd(), directory);
|
|
37
41
|
if (existsSync(targetDir) &&
|
|
38
42
|
readdirSync(targetDir).length > 0 &&
|
|
@@ -40,10 +44,26 @@ export function registerInitCommand(parent) {
|
|
|
40
44
|
failWith(`Directory ${directory} is not empty. Pass --force to scaffold into it anyway.`, { code: ExitCodes.GenericError });
|
|
41
45
|
}
|
|
42
46
|
const appName = opts.name !== undefined ? opts.name : basename(targetDir);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
if (opts.from !== undefined) {
|
|
48
|
+
const ref = parseFromRef(opts.from);
|
|
49
|
+
if (ref === undefined) {
|
|
50
|
+
failWith(`Could not parse --from "${opts.from}". Expected owner/repo[/folder][@ref] or a github.com URL.`, { code: ExitCodes.GenericError });
|
|
51
|
+
}
|
|
52
|
+
const copied = await scaffoldFromGitHub({ ref, targetDir, appName });
|
|
53
|
+
const detail = copied.length > 0 ? ` (${copied.join(", ")})` : "";
|
|
54
|
+
success(`Scaffolded ${directory} from ${ref.owner}/${ref.repo}${ref.subPath === "" ? "" : `/${ref.subPath}`}${detail}`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const templateDir = join(templatesRoot, opts.template);
|
|
58
|
+
if (existsSync(templateDir) === false) {
|
|
59
|
+
const slugs = await listTemplates(templatesRoot);
|
|
60
|
+
failWith(`Unknown template "${opts.template}". Available: ${slugs.join(", ")}`, { code: ExitCodes.GenericError });
|
|
61
|
+
}
|
|
62
|
+
await copyDirectory(templateDir, targetDir, [
|
|
63
|
+
{ from: "__APP_NAME__", to: appName },
|
|
64
|
+
]);
|
|
65
|
+
success(`Scaffolded ${directory} from the "${opts.template}" template`);
|
|
66
|
+
}
|
|
47
67
|
info(``);
|
|
48
68
|
info([
|
|
49
69
|
`Next steps:`,
|
|
@@ -62,15 +82,19 @@ export function registerInitCommand(parent) {
|
|
|
62
82
|
// unrelated `templates/` dir along the way). Works both from the monorepo build
|
|
63
83
|
// output and an installed `node_modules/@cargo-ai/cdk`.
|
|
64
84
|
function findTemplatesRoot() {
|
|
65
|
-
|
|
66
|
-
for (
|
|
85
|
+
const start = dirname(fileURLToPath(import.meta.url));
|
|
86
|
+
for (const cursor of ancestorDirs(start, 8)) {
|
|
67
87
|
const candidate = join(cursor, "templates");
|
|
68
88
|
if (existsSync(join(candidate, "blank")))
|
|
69
89
|
return candidate;
|
|
70
|
-
const parent = dirname(cursor);
|
|
71
|
-
if (parent === cursor)
|
|
72
|
-
break;
|
|
73
|
-
cursor = parent;
|
|
74
90
|
}
|
|
75
91
|
throw new Error("Could not locate the @cargo-ai/cdk templates directory. Reinstall @cargo-ai/cdk if this persists.");
|
|
76
92
|
}
|
|
93
|
+
// The directory chain from `start` walking up to `max` levels, stopping at the
|
|
94
|
+
// filesystem root — so a marker file/dir can be located independent of build depth.
|
|
95
|
+
function ancestorDirs(start, max) {
|
|
96
|
+
const parent = dirname(start);
|
|
97
|
+
if (max <= 1 || parent === start)
|
|
98
|
+
return [start];
|
|
99
|
+
return [start, ...ancestorDirs(parent, max - 1)];
|
|
100
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inputTypes.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/inputTypes.ts"],"names":[],"mappings":"AAmCA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,QAE+D,CAAC;AAEjG;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAM5D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAQ3D;
|
|
1
|
+
{"version":3,"file":"inputTypes.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/inputTypes.ts"],"names":[],"mappings":"AAmCA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,QAE+D,CAAC;AAEjG;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAM5D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAQ3D;AAmTD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAU5E"}
|
|
@@ -83,10 +83,20 @@ function printSchema(schema, topLevel, encRef) {
|
|
|
83
83
|
// Connectors model "pick an auth method, then its fields" as an object with
|
|
84
84
|
// a discriminant property plus `allOf: [{ if, then, else }]`. Expand that to
|
|
85
85
|
// a discriminated union (e.g. HubSpot → `{ method: "privateApp"; … } | { …
|
|
86
|
-
// "oauth"; … }`)
|
|
86
|
+
// "oauth"; … }`).
|
|
87
87
|
const conditional = printConditionalObject(schema, topLevel, encRef);
|
|
88
88
|
if (conditional !== undefined)
|
|
89
89
|
return conditional;
|
|
90
|
+
// General case: expand the `allOf` into the distinct object shapes it allows
|
|
91
|
+
// (see `flattenAllOf`) and render them as a union — e.g. Slack postMessage →
|
|
92
|
+
// `{ channelId; … format: "markdown"; body } | { … format: "blockKit";
|
|
93
|
+
// blocks }`. This keeps the base props AND the variant members instead of
|
|
94
|
+
// dropping either.
|
|
95
|
+
const shapes = flattenAllOf(schema);
|
|
96
|
+
if (shapes.length > 0) {
|
|
97
|
+
return uniqueUnion(shapes.map((s) => printObjectShape(s, topLevel, encRef)));
|
|
98
|
+
}
|
|
99
|
+
// Last resort: render the head (never abort codegen on an odd schema).
|
|
90
100
|
return printSchema(schema.allOf[0], topLevel, encRef);
|
|
91
101
|
}
|
|
92
102
|
if (typeof schema.type !== "string")
|
|
@@ -120,10 +130,14 @@ function printConditionalObject(schema, topLevel, encRef) {
|
|
|
120
130
|
if (props === undefined || schema.allOf === undefined)
|
|
121
131
|
return undefined;
|
|
122
132
|
const block = schema.allOf.find((a) => a.if !== undefined);
|
|
123
|
-
if (block === undefined
|
|
133
|
+
if (block === undefined)
|
|
134
|
+
return undefined;
|
|
135
|
+
const blockIf = block.if;
|
|
136
|
+
if (blockIf === undefined || blockIf.properties === undefined) {
|
|
124
137
|
return undefined;
|
|
138
|
+
}
|
|
125
139
|
// The discriminant: the single property the `if` tests, and its `const` value.
|
|
126
|
-
const ifEntries = Object.entries(
|
|
140
|
+
const ifEntries = Object.entries(blockIf.properties);
|
|
127
141
|
if (ifEntries.length !== 1)
|
|
128
142
|
return undefined;
|
|
129
143
|
const [discKey, discCond] = ifEntries[0];
|
|
@@ -143,11 +157,11 @@ function printConditionalObject(schema, topLevel, encRef) {
|
|
|
143
157
|
...props,
|
|
144
158
|
[discKey]: { const: value },
|
|
145
159
|
};
|
|
146
|
-
if (extra
|
|
160
|
+
if (extra !== undefined && extra.properties !== undefined) {
|
|
147
161
|
Object.assign(branchProps, extra.properties);
|
|
148
162
|
}
|
|
149
163
|
const required = new Set(baseRequired);
|
|
150
|
-
if (Array.isArray(extra
|
|
164
|
+
if (extra !== undefined && Array.isArray(extra.required)) {
|
|
151
165
|
for (const r of extra.required)
|
|
152
166
|
required.add(r);
|
|
153
167
|
}
|
|
@@ -162,7 +176,7 @@ function discriminantValues(schema) {
|
|
|
162
176
|
return [schema.const];
|
|
163
177
|
if (Array.isArray(schema.enum))
|
|
164
178
|
return schema.enum;
|
|
165
|
-
const variants = schema.oneOf
|
|
179
|
+
const variants = schema.oneOf !== undefined ? schema.oneOf : schema.anyOf;
|
|
166
180
|
if (Array.isArray(variants)) {
|
|
167
181
|
const consts = variants.map((v) => v.const).filter((v) => v !== undefined);
|
|
168
182
|
if (consts.length > 0)
|
|
@@ -170,6 +184,63 @@ function discriminantValues(schema) {
|
|
|
170
184
|
}
|
|
171
185
|
return [];
|
|
172
186
|
}
|
|
187
|
+
function flattenAllOf(schema) {
|
|
188
|
+
const { allOf = [] } = schema;
|
|
189
|
+
const base = objectShapeOf(schema);
|
|
190
|
+
const seed = base === undefined ? emptyShape() : base;
|
|
191
|
+
return allOf.map(shapesOf).reduce(crossMerge, [seed]);
|
|
192
|
+
}
|
|
193
|
+
// The object shape(s) a single allOf member contributes: one for a plain object,
|
|
194
|
+
// several for an `anyOf`/`oneOf`, or a nested `allOf`'s result. Empty when the
|
|
195
|
+
// member (or any `anyOf`/`oneOf` variant) isn't object-shaped.
|
|
196
|
+
function shapesOf(member) {
|
|
197
|
+
const { allOf, anyOf, oneOf } = member;
|
|
198
|
+
if (allOf !== undefined && allOf.length > 0) {
|
|
199
|
+
return flattenAllOf(member);
|
|
200
|
+
}
|
|
201
|
+
const variants = anyOf !== undefined ? anyOf : oneOf;
|
|
202
|
+
if (variants !== undefined && variants.length > 0) {
|
|
203
|
+
const shapes = variants
|
|
204
|
+
.map(objectShapeOf)
|
|
205
|
+
.filter((shape) => shape !== undefined);
|
|
206
|
+
// A non-object variant drops out above; if any did, the union is incomplete
|
|
207
|
+
// so give up on the whole member rather than emit a narrower type.
|
|
208
|
+
return shapes.length === variants.length ? shapes : [];
|
|
209
|
+
}
|
|
210
|
+
const shape = objectShapeOf(member);
|
|
211
|
+
return shape === undefined ? [] : [shape];
|
|
212
|
+
}
|
|
213
|
+
// Every current shape merged with every option: {a} × [{b}, {c}] → [{ab}, {ac}].
|
|
214
|
+
function crossMerge(shapes, options) {
|
|
215
|
+
return shapes.flatMap((shape) => options.map((option) => mergeShapes(shape, option)));
|
|
216
|
+
}
|
|
217
|
+
// Read a schema as an object shape (its properties + required), or undefined
|
|
218
|
+
// when it isn't object-shaped.
|
|
219
|
+
function objectShapeOf(schema) {
|
|
220
|
+
const { properties, type, required } = schema;
|
|
221
|
+
if (properties === undefined && type !== "object")
|
|
222
|
+
return undefined;
|
|
223
|
+
return {
|
|
224
|
+
properties: { ...properties },
|
|
225
|
+
required: new Set(Array.isArray(required) ? required : []),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function mergeShapes(a, b) {
|
|
229
|
+
return {
|
|
230
|
+
properties: { ...a.properties, ...b.properties },
|
|
231
|
+
required: new Set([...a.required, ...b.required]),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function emptyShape() {
|
|
235
|
+
return { properties: {}, required: new Set() };
|
|
236
|
+
}
|
|
237
|
+
function printObjectShape(shape, topLevel, encRef) {
|
|
238
|
+
return printObject({
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: shape.properties,
|
|
241
|
+
required: [...shape.required],
|
|
242
|
+
}, topLevel, encRef);
|
|
243
|
+
}
|
|
173
244
|
function printArray(schema, encRef) {
|
|
174
245
|
if (schema.items === undefined)
|
|
175
246
|
return "unknown[]";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiHzC,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI,CAgE7E"}
|