@animaapp/cli 0.7.1 → 0.9.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/README.md +307 -57
- package/dist/index.js +1261 -145
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command23 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/create.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -176,7 +176,19 @@ function validationError(message, detail) {
|
|
|
176
176
|
);
|
|
177
177
|
}
|
|
178
178
|
function apiError(message, detail) {
|
|
179
|
-
|
|
179
|
+
const text = message.replace(/^Error: /, "");
|
|
180
|
+
return new CliError("API_ERROR", withWorkspaceHint(text), EXIT_CODES.API_ERROR, detail);
|
|
181
|
+
}
|
|
182
|
+
function withWorkspaceHint(message) {
|
|
183
|
+
if (/\bworkspaceId\b/.test(message)) {
|
|
184
|
+
return `${message}
|
|
185
|
+
Pass it with --workspace <id>; \`anima workspaces\` lists the ids.`;
|
|
186
|
+
}
|
|
187
|
+
if (message === "Workspace not found") {
|
|
188
|
+
return `${message}
|
|
189
|
+
\`anima workspaces\` lists the workspaces you can reach, with their ids.`;
|
|
190
|
+
}
|
|
191
|
+
return message;
|
|
180
192
|
}
|
|
181
193
|
function timeoutError() {
|
|
182
194
|
return new CliError(
|
|
@@ -423,9 +435,16 @@ import { CallToolResultSchema, ErrorCode } from "@modelcontextprotocol/sdk/types
|
|
|
423
435
|
import { createRequire } from "module";
|
|
424
436
|
var { version } = createRequire(import.meta.url)("../package.json");
|
|
425
437
|
|
|
438
|
+
// src/clientIdentity.ts
|
|
439
|
+
var ANIMA_CLIENT_IDENTITY = {
|
|
440
|
+
type: "cli",
|
|
441
|
+
name: "anima-cli",
|
|
442
|
+
version
|
|
443
|
+
};
|
|
444
|
+
|
|
426
445
|
// src/api/mcp.ts
|
|
427
446
|
var MCP_PATH = "/v1/mcp";
|
|
428
|
-
var CLIENT_INFO = { name:
|
|
447
|
+
var CLIENT_INFO = { name: ANIMA_CLIENT_IDENTITY.name, version: ANIMA_CLIENT_IDENTITY.version };
|
|
429
448
|
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
430
449
|
var AnimaMcpClient = class {
|
|
431
450
|
#client;
|
|
@@ -433,7 +452,10 @@ var AnimaMcpClient = class {
|
|
|
433
452
|
#url;
|
|
434
453
|
constructor(config) {
|
|
435
454
|
const headers = {
|
|
436
|
-
Authorization: `Bearer ${config.token}
|
|
455
|
+
Authorization: `Bearer ${config.token}`,
|
|
456
|
+
"X-Anima-Client-Type": ANIMA_CLIENT_IDENTITY.type,
|
|
457
|
+
"X-Anima-Client-Name": ANIMA_CLIENT_IDENTITY.name,
|
|
458
|
+
"X-Anima-Client-Version": ANIMA_CLIENT_IDENTITY.version
|
|
437
459
|
};
|
|
438
460
|
if (config.teamId) headers["X-Team-Id"] = config.teamId;
|
|
439
461
|
if (config.figmaToken) headers["X-Figma-Token"] = config.figmaToken;
|
|
@@ -475,6 +497,11 @@ var AnimaMcpClient = class {
|
|
|
475
497
|
if (err?.code === ErrorCode.RequestTimeout) {
|
|
476
498
|
throw timeoutError();
|
|
477
499
|
}
|
|
500
|
+
if (err?.code === ErrorCode.InvalidParams && /\bnot found\b/.test(err.message)) {
|
|
501
|
+
throw apiError(
|
|
502
|
+
`${name} is not available to this credential. An agent is offered it only once a human has granted it the access it needs in a workspace of this team (read to list, write to create).`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
478
505
|
throw err;
|
|
479
506
|
}
|
|
480
507
|
logEvent("mcp_tool_result", { tool: name, isError: result.isError ?? false });
|
|
@@ -608,8 +635,11 @@ function printSessionResult(result) {
|
|
|
608
635
|
console.log();
|
|
609
636
|
printSuccess("Artifact created successfully!");
|
|
610
637
|
printKeyValue("Session ID", result.sessionId);
|
|
638
|
+
if (result.artifactUrl) {
|
|
639
|
+
printKeyValue("Your artifact", chalk.cyan.underline(result.artifactUrl));
|
|
640
|
+
}
|
|
611
641
|
if (result.playgroundUrl) {
|
|
612
|
-
printKeyValue("
|
|
642
|
+
printKeyValue("Edit in Playground", chalk.cyan.underline(result.playgroundUrl));
|
|
613
643
|
}
|
|
614
644
|
if (result.tokenUsage) {
|
|
615
645
|
printKeyValue("Token usage", result.tokenUsage.toLocaleString());
|
|
@@ -710,8 +740,8 @@ function zipFolder(root) {
|
|
|
710
740
|
});
|
|
711
741
|
return zip.toBuffer();
|
|
712
742
|
}
|
|
713
|
-
function isProbablyBinary(
|
|
714
|
-
if (BINARY_EXTENSIONS.has(extname(
|
|
743
|
+
function isProbablyBinary(path7, buffer) {
|
|
744
|
+
if (BINARY_EXTENSIONS.has(extname(path7).toLowerCase())) return true;
|
|
715
745
|
return buffer.includes(0);
|
|
716
746
|
}
|
|
717
747
|
function collectProjectSource(fromPath) {
|
|
@@ -732,30 +762,33 @@ function collectProjectSource(fromPath) {
|
|
|
732
762
|
}
|
|
733
763
|
const files = {};
|
|
734
764
|
let totalBytes = 0;
|
|
735
|
-
for (const
|
|
736
|
-
const buffer = readFileSync(join(fromPath,
|
|
737
|
-
if (isProbablyBinary(
|
|
738
|
-
return { kind: "zip", buffer: zipFolder(fromPath), reason: `binary file present (${
|
|
765
|
+
for (const path7 of paths) {
|
|
766
|
+
const buffer = readFileSync(join(fromPath, path7));
|
|
767
|
+
if (isProbablyBinary(path7, buffer)) {
|
|
768
|
+
return { kind: "zip", buffer: zipFolder(fromPath), reason: `binary file present (${path7})` };
|
|
739
769
|
}
|
|
740
770
|
totalBytes += buffer.length;
|
|
741
771
|
if (totalBytes > MAX_INLINE_BYTES) {
|
|
742
772
|
return { kind: "zip", buffer: zipFolder(fromPath), reason: "project exceeds the inline size limit" };
|
|
743
773
|
}
|
|
744
|
-
files[
|
|
774
|
+
files[path7.split("\\").join("/")] = buffer.toString("utf8");
|
|
745
775
|
}
|
|
746
776
|
return { kind: "files", files, fileCount: paths.length };
|
|
747
777
|
}
|
|
748
778
|
|
|
749
|
-
// src/utils/
|
|
750
|
-
var
|
|
751
|
-
function
|
|
779
|
+
// src/utils/artifactUrl.ts
|
|
780
|
+
var ARTIFACT_BASE = "https://app.agentgrid.io/artifacts";
|
|
781
|
+
function artifactUrlFor(sessionId) {
|
|
782
|
+
return `${ARTIFACT_BASE}/${sessionId}`;
|
|
783
|
+
}
|
|
784
|
+
function parseArtifactUrl(input) {
|
|
752
785
|
if (!input.includes("/")) {
|
|
753
786
|
return input;
|
|
754
787
|
}
|
|
755
788
|
try {
|
|
756
789
|
const url = new URL(input);
|
|
757
790
|
const parts = url.pathname.split("/").filter(Boolean);
|
|
758
|
-
const markerIndex = parts.findIndex((p) => p === "
|
|
791
|
+
const markerIndex = parts.findIndex((p) => p === "artifacts" || p === "chat");
|
|
759
792
|
if (markerIndex !== -1 && markerIndex + 1 < parts.length) {
|
|
760
793
|
return parts[markerIndex + 1];
|
|
761
794
|
}
|
|
@@ -765,11 +798,63 @@ function parsePlaygroundUrl(input) {
|
|
|
765
798
|
}
|
|
766
799
|
}
|
|
767
800
|
|
|
801
|
+
// src/utils/waitForArtifact.ts
|
|
802
|
+
var STATUS_CALL_TIMEOUT_MS = 12e4;
|
|
803
|
+
async function fetchArtifactStatus(client, sessionId, options = {}) {
|
|
804
|
+
return client.callTool(
|
|
805
|
+
"artifact-status",
|
|
806
|
+
{ sessionId, wait: options.wait ?? false },
|
|
807
|
+
{
|
|
808
|
+
timeoutMs: STATUS_CALL_TIMEOUT_MS,
|
|
809
|
+
onProgress: options.onProgress ? (p) => options.onProgress(p.message ?? `Generating (${Math.round(p.progress * 100)}%)...`) : void 0
|
|
810
|
+
}
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
async function waitForArtifact(client, sessionId, options = { deadline: Date.now() }) {
|
|
814
|
+
for (; ; ) {
|
|
815
|
+
const result = await fetchArtifactStatus(client, sessionId, {
|
|
816
|
+
wait: true,
|
|
817
|
+
onProgress: options.onProgress
|
|
818
|
+
});
|
|
819
|
+
if (!result.success) {
|
|
820
|
+
throw new CliError(
|
|
821
|
+
"API_ERROR",
|
|
822
|
+
result.reason || result.error || "Could not read the artifact's status",
|
|
823
|
+
EXIT_CODES.API_ERROR,
|
|
824
|
+
result
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
if (result.status !== "generating") {
|
|
828
|
+
logEvent("generation_settled", { sessionId, status: result.status });
|
|
829
|
+
return result;
|
|
830
|
+
}
|
|
831
|
+
if (Date.now() >= options.deadline) {
|
|
832
|
+
logEvent("generation_wait_timeout", { sessionId, progress: result.progress });
|
|
833
|
+
throw generationTimeoutError(sessionId, result.progress);
|
|
834
|
+
}
|
|
835
|
+
options.onProgress?.(
|
|
836
|
+
result.progress !== void 0 ? `Generating (${result.progress}%)...` : "Generating..."
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function generationTimeoutError(sessionId, progress) {
|
|
841
|
+
const reached = progress !== void 0 ? ` (reached ${progress}%)` : "";
|
|
842
|
+
return new CliError(
|
|
843
|
+
"TIMEOUT",
|
|
844
|
+
`Still generating${reached} when the wait budget ran out. Generation is NOT cancelled \u2014 it keeps running in the background.
|
|
845
|
+
Pick the wait back up with: anima status ${sessionId} --wait
|
|
846
|
+
Or allow longer next time with --timeout <ms>.`,
|
|
847
|
+
EXIT_CODES.TIMEOUT,
|
|
848
|
+
{ sessionId, progress }
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
768
852
|
// src/api/anonymousCreate.ts
|
|
769
853
|
async function createAnonymousArtifact(request, fetchFn = fetch) {
|
|
770
854
|
const url = `${request.apiUrl.replace(/\/$/, "")}/v1/generationSessions`;
|
|
771
855
|
const body = {
|
|
772
856
|
type: "import",
|
|
857
|
+
clientType: ANIMA_CLIENT_IDENTITY.type,
|
|
773
858
|
...request.files ? { files: request.files } : {},
|
|
774
859
|
...request.zipUploadId ? { zipUploadId: request.zipUploadId } : {},
|
|
775
860
|
...request.framework ? { framework: request.framework } : {},
|
|
@@ -810,8 +895,16 @@ async function getAnonymousZipUploadUrl(apiUrl, fetchFn = fetch) {
|
|
|
810
895
|
return { zipUploadId: payload.zipUploadId, uploadUrl: payload.uploadUrl };
|
|
811
896
|
}
|
|
812
897
|
|
|
898
|
+
// src/utils/workspaceFlag.ts
|
|
899
|
+
function workspaceFlag(value) {
|
|
900
|
+
if (value !== void 0 && value.trim() === "") {
|
|
901
|
+
throw validationError("--workspace needs a workspace id; `anima workspaces` lists them");
|
|
902
|
+
}
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
|
|
813
906
|
// src/commands/create.ts
|
|
814
|
-
var createCommand = new Command("create").description(
|
|
907
|
+
var createCommand = new Command("create").summary("Create an artifact: your code, an empty repo, or AI generation").description(
|
|
815
908
|
"Create an artifact from your own code: empty for code you'll push (instant) or import your existing code (instant). Also supports AI generation: p2c/l2c/f2c (3-7 min)."
|
|
816
909
|
).requiredOption(
|
|
817
910
|
"-t, --type <type>",
|
|
@@ -822,13 +915,19 @@ var createCommand = new Command("create").description(
|
|
|
822
915
|
).option("--styling <style>", "tailwind | css | plain_css | css_modules | inline_styles", "tailwind").option("--language <lang>", "typescript | javascript").option("--ui-library <lib>", "mui | antd | shadcn | clean_react").option("--guidelines <text>", "Additional constraints (p2c only)").option(
|
|
823
916
|
"--artifact-type <kind>",
|
|
824
917
|
"app | markdown \u2014 what the artifact IS. app (default) needs an index.html; markdown needs .md files"
|
|
825
|
-
).option("--name <name>", "Project name (empty/import only)").option(
|
|
918
|
+
).option("--name <name>", "Project name (empty/import only)").option(
|
|
919
|
+
"-w, --workspace <id>",
|
|
920
|
+
"The workspace to create it in (id from `anima workspaces`). Optional when you can create in only one workspace; otherwise required, since there is no default"
|
|
921
|
+
).option("--from <path>", "Folder or .zip with your project (import only) \u2014 becomes the first commit").option(
|
|
826
922
|
"--anonymous",
|
|
827
923
|
"Create without an account (import only). Returns a claim link to send a human, and a handoff token to log in with once they claim it"
|
|
828
924
|
).option(
|
|
829
925
|
"--handoff <token>",
|
|
830
926
|
"Add this artifact to an existing claim (--anonymous only), so one claim link covers them all. Defaults to the token from your last anonymous create"
|
|
831
|
-
).option("--client-name <name>", "How this CLI is credited as the creator of an anonymous artifact", "AgentGrid CLI").option("--json", "Output as JSON").option(
|
|
927
|
+
).option("--client-name <name>", "How this CLI is credited as the creator of an anonymous artifact", "AgentGrid CLI").option("--json", "Output as JSON").option(
|
|
928
|
+
"--no-wait",
|
|
929
|
+
"Return as soon as generation starts (p2c/l2c/f2c), instead of waiting for the finished app. Poll with `anima status <sessionId> --wait`"
|
|
930
|
+
).option("--timeout <ms>", "Timeout in milliseconds", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
|
|
832
931
|
const mode = detectOutputMode(opts);
|
|
833
932
|
initLogger(opts.logFile);
|
|
834
933
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -872,22 +971,44 @@ ${grant.nextSteps}` : message, grant);
|
|
|
872
971
|
progress.start(
|
|
873
972
|
type === "empty" ? "Creating empty artifact..." : type === "import" ? "Importing your project..." : `Creating ${type.toUpperCase()} artifact...`
|
|
874
973
|
);
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
}
|
|
881
|
-
|
|
974
|
+
const timeoutMs = parseInt(opts.timeout, 10);
|
|
975
|
+
const deadline = Date.now() + timeoutMs;
|
|
976
|
+
const { result, settled } = await withMcp(mcpConfig, async (client) => {
|
|
977
|
+
const created = await client.callTool("artifact-create", args, { timeoutMs });
|
|
978
|
+
if (!created.success || !created.sessionId) return { created };
|
|
979
|
+
if (created.status !== "generating" || !opts.wait) return { created };
|
|
980
|
+
progress.update("Generating your app...");
|
|
981
|
+
const status2 = await waitForArtifact(client, created.sessionId, {
|
|
982
|
+
deadline,
|
|
983
|
+
onProgress: (message) => progress.update(message)
|
|
984
|
+
});
|
|
985
|
+
return { created, status: status2 };
|
|
986
|
+
}).then(({ created, status: status2 }) => ({ result: created, settled: status2 }));
|
|
882
987
|
if (!result.success || !result.sessionId) {
|
|
883
|
-
const message = result.reason || result.error || "Generation failed";
|
|
988
|
+
const message = result.message || result.reason || result.error || "Generation failed";
|
|
884
989
|
throw apiError(result.nextSteps ? `${message}
|
|
885
990
|
${result.nextSteps}` : message, result);
|
|
886
991
|
}
|
|
887
|
-
|
|
888
|
-
|
|
992
|
+
if (settled?.status === "failed") {
|
|
993
|
+
throw apiError(settled.error || "Generation failed", settled);
|
|
994
|
+
}
|
|
995
|
+
const artifactUrl = settled?.artifactUrl || result.artifactUrl || artifactUrlFor(result.sessionId);
|
|
996
|
+
const playgroundUrl = settled?.playgroundUrl ?? result.playgroundUrl;
|
|
997
|
+
const previewUrl = settled?.previewUrl ?? result.previewUrl;
|
|
998
|
+
const status = settled?.status ?? result.status ?? "ready";
|
|
999
|
+
progress.succeed(settled ? "Your app is ready!" : "Artifact created!");
|
|
889
1000
|
if (mode === "pretty") {
|
|
890
|
-
printSessionResult({
|
|
1001
|
+
printSessionResult({
|
|
1002
|
+
sessionId: result.sessionId,
|
|
1003
|
+
artifactUrl,
|
|
1004
|
+
playgroundUrl
|
|
1005
|
+
});
|
|
1006
|
+
if (status === "generating") {
|
|
1007
|
+
console.log(
|
|
1008
|
+
` Still generating. Wait for it with ${chalk2.cyan(`anima status ${result.sessionId} --wait`)}.`
|
|
1009
|
+
);
|
|
1010
|
+
console.log();
|
|
1011
|
+
}
|
|
891
1012
|
if (result.gitRemoteUrl) {
|
|
892
1013
|
console.log(` ${chalk2.cyan(`git clone ${result.gitRemoteUrl}`)}`);
|
|
893
1014
|
console.log();
|
|
@@ -920,7 +1041,14 @@ ${result.nextSteps}` : message, result);
|
|
|
920
1041
|
outputResult(mode, {
|
|
921
1042
|
success: true,
|
|
922
1043
|
sessionId: result.sessionId,
|
|
923
|
-
|
|
1044
|
+
status,
|
|
1045
|
+
artifactUrl,
|
|
1046
|
+
...playgroundUrl ? { playgroundUrl } : {},
|
|
1047
|
+
...previewUrl ? { previewUrl } : {},
|
|
1048
|
+
...settled?.name ? { name: settled.name } : {},
|
|
1049
|
+
...status === "generating" ? {
|
|
1050
|
+
nextSteps: `Generation is still running. Wait for it with: anima status ${result.sessionId} --wait`
|
|
1051
|
+
} : {},
|
|
924
1052
|
...result.gitRemoteUrl ? { gitRemoteUrl: result.gitRemoteUrl, access: result.access, expiresAt: result.expiresAt } : {},
|
|
925
1053
|
...result.framework ? { framework: result.framework } : {},
|
|
926
1054
|
...result.fileCount !== void 0 ? { fileCount: result.fileCount } : {},
|
|
@@ -1063,6 +1191,12 @@ Log in first for the other types: anima login`
|
|
|
1063
1191
|
if (type !== "import" && opts.from) {
|
|
1064
1192
|
throw validationError("--from is only valid with -t import");
|
|
1065
1193
|
}
|
|
1194
|
+
workspaceFlag(opts.workspace);
|
|
1195
|
+
if (opts.workspace !== void 0 && opts.anonymous) {
|
|
1196
|
+
throw validationError(
|
|
1197
|
+
"--workspace does not apply to --anonymous creates \u2014 they belong to no team until a human claims them"
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1066
1200
|
}
|
|
1067
1201
|
function resolveFigmaToken(opts, stored) {
|
|
1068
1202
|
const figmaToken = opts.figmaToken || process.env.FIGMA_TOKEN || stored;
|
|
@@ -1081,7 +1215,8 @@ function buildCreateArgs(type, opts) {
|
|
|
1081
1215
|
// import is what made markdown artifacts render as empty apps.
|
|
1082
1216
|
...opts.framework && opts.artifactType !== "markdown" ? { framework: opts.framework } : {},
|
|
1083
1217
|
...opts.artifactType ? { artifactType: opts.artifactType } : {},
|
|
1084
|
-
...opts.name ? { name: opts.name } : {}
|
|
1218
|
+
...opts.name ? { name: opts.name } : {},
|
|
1219
|
+
...opts.workspace !== void 0 ? { workspaceId: opts.workspace } : {}
|
|
1085
1220
|
};
|
|
1086
1221
|
}
|
|
1087
1222
|
const framework = opts.framework ?? "html";
|
|
@@ -1096,6 +1231,7 @@ function buildCreateArgs(type, opts) {
|
|
|
1096
1231
|
};
|
|
1097
1232
|
if (language) args.language = language;
|
|
1098
1233
|
if (opts.uiLibrary) args.uiLibrary = opts.uiLibrary;
|
|
1234
|
+
if (opts.workspace !== void 0) args.workspaceId = opts.workspace;
|
|
1099
1235
|
switch (type) {
|
|
1100
1236
|
case "p2c":
|
|
1101
1237
|
args.prompt = opts.prompt;
|
|
@@ -1120,22 +1256,107 @@ function buildCreateArgs(type, opts) {
|
|
|
1120
1256
|
return args;
|
|
1121
1257
|
}
|
|
1122
1258
|
|
|
1123
|
-
// src/commands/
|
|
1259
|
+
// src/commands/createKnowledge.ts
|
|
1124
1260
|
import { Command as Command2 } from "commander";
|
|
1261
|
+
|
|
1262
|
+
// src/commands/runner.ts
|
|
1263
|
+
async function withCommandErrors(mode, fn) {
|
|
1264
|
+
try {
|
|
1265
|
+
await fn();
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
if (err instanceof CliError) {
|
|
1268
|
+
if (mode === "pretty") {
|
|
1269
|
+
printError(err.message);
|
|
1270
|
+
} else {
|
|
1271
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1272
|
+
}
|
|
1273
|
+
process.exit(err.exitCode);
|
|
1274
|
+
}
|
|
1275
|
+
throw err;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/commands/createKnowledge.ts
|
|
1280
|
+
function buildCreateKnowledgeArgs(name, workspaceId) {
|
|
1281
|
+
if (name !== void 0 && name.length > 120) {
|
|
1282
|
+
throw validationError("--name must be at most 120 characters");
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
...name === void 0 ? {} : { name },
|
|
1286
|
+
...workspaceFlag(workspaceId) === void 0 ? {} : { workspaceId }
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function makeCreateKnowledgeCommand() {
|
|
1290
|
+
return new Command2("create-knowledge").summary("Create a knowledge artifact from Anima's template").description(
|
|
1291
|
+
"Create a ready knowledge artifact from Anima's server-provided template. The template supplies the files and framework; only an optional display name is accepted."
|
|
1292
|
+
).option("--name <name>", 'Display name (defaults to "Untitled project"; max 120 characters)').option(
|
|
1293
|
+
"-w, --workspace <id>",
|
|
1294
|
+
"The workspace to create it in (id from `anima workspaces`). Optional when you can create in only one workspace; otherwise required, since there is no default"
|
|
1295
|
+
).option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
|
|
1296
|
+
const mode = detectOutputMode(opts);
|
|
1297
|
+
initLogger(opts.logFile);
|
|
1298
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
1299
|
+
await withCommandErrors(mode, async () => {
|
|
1300
|
+
try {
|
|
1301
|
+
await run();
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
progress.fail("Failed");
|
|
1304
|
+
throw err;
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
async function run() {
|
|
1308
|
+
const args = buildCreateKnowledgeArgs(opts.name, opts.workspace);
|
|
1309
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
1310
|
+
progress.start("Creating knowledge artifact from template...");
|
|
1311
|
+
const result = await withMcp(
|
|
1312
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1313
|
+
(client) => client.callTool("artifact-create_knowledge", args)
|
|
1314
|
+
);
|
|
1315
|
+
if (!result.success || !result.sessionId) {
|
|
1316
|
+
throw apiError(result.message || result.reason || result.error || "Knowledge artifact creation failed", result);
|
|
1317
|
+
}
|
|
1318
|
+
progress.succeed("Knowledge artifact created");
|
|
1319
|
+
if (mode === "pretty") {
|
|
1320
|
+
printSessionResult({
|
|
1321
|
+
sessionId: result.sessionId,
|
|
1322
|
+
artifactUrl: result.artifactUrl,
|
|
1323
|
+
playgroundUrl: result.playgroundUrl
|
|
1324
|
+
});
|
|
1325
|
+
if (result.fileCount !== void 0) {
|
|
1326
|
+
printKeyValue("Template files", String(result.fileCount));
|
|
1327
|
+
}
|
|
1328
|
+
console.log();
|
|
1329
|
+
} else {
|
|
1330
|
+
outputResult(mode, result);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
var createKnowledgeCommand = makeCreateKnowledgeCommand();
|
|
1336
|
+
|
|
1337
|
+
// src/commands/list.ts
|
|
1338
|
+
import { Command as Command3 } from "commander";
|
|
1125
1339
|
import chalk3 from "chalk";
|
|
1126
|
-
var listCommand = new
|
|
1127
|
-
"List
|
|
1340
|
+
var listCommand = new Command3("list").summary("List the artifacts you can read, most-recently-updated first").aliases(["ls", "artifacts"]).description(
|
|
1341
|
+
"List the artifacts you can read, across every workspace you can reach or in one (most-recently-updated first), so you can resume existing work."
|
|
1342
|
+
).option(
|
|
1343
|
+
"-w, --workspace <id>",
|
|
1344
|
+
"Only this workspace (id from `anima workspaces`); omit to span every workspace you can read"
|
|
1128
1345
|
).option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
|
|
1129
1346
|
const mode = detectOutputMode(opts);
|
|
1130
1347
|
initLogger(opts.logFile);
|
|
1131
1348
|
try {
|
|
1132
1349
|
const auth = await resolveAuth(opts.apiUrl);
|
|
1350
|
+
const workspaceId = workspaceFlag(opts.workspace);
|
|
1133
1351
|
const result = await withMcp(
|
|
1134
1352
|
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1135
|
-
(client) => client.callTool(
|
|
1353
|
+
(client) => client.callTool(
|
|
1354
|
+
"workspace-list_artifacts",
|
|
1355
|
+
workspaceId !== void 0 ? { workspaceId } : {}
|
|
1356
|
+
)
|
|
1136
1357
|
);
|
|
1137
1358
|
if (!result.success) {
|
|
1138
|
-
const message = result.reason || result.error || "Could not list artifacts";
|
|
1359
|
+
const message = result.message || result.reason || result.error || "Could not list artifacts";
|
|
1139
1360
|
throw apiError(message, result);
|
|
1140
1361
|
}
|
|
1141
1362
|
const artifacts = result.artifacts ?? [];
|
|
@@ -1144,22 +1365,19 @@ var listCommand = new Command2("list").aliases(["ls", "artifacts"]).description(
|
|
|
1144
1365
|
if (artifacts.length === 0) {
|
|
1145
1366
|
console.log();
|
|
1146
1367
|
console.log(
|
|
1147
|
-
" No artifacts
|
|
1368
|
+
workspaceId !== void 0 ? " Nothing you can read in that workspace." : " No artifacts you can read \u2014 create one with `create -t empty` or `create -t import`."
|
|
1148
1369
|
);
|
|
1149
1370
|
console.log();
|
|
1150
1371
|
} else {
|
|
1151
1372
|
console.log();
|
|
1152
1373
|
for (const a of artifacts) {
|
|
1153
1374
|
console.log(` ${chalk3.bold(a.name)} ${chalk3.gray(`(${a.type})`)}`);
|
|
1154
|
-
|
|
1155
|
-
|
|
1375
|
+
console.log(` ${chalk3.gray("session:")} ${a.sessionId}`);
|
|
1376
|
+
if (a.workspaceName) {
|
|
1377
|
+
console.log(` ${chalk3.gray("workspace:")} ${a.workspaceName}`);
|
|
1156
1378
|
}
|
|
1157
1379
|
console.log(` ${chalk3.gray("updated:")} ${formatUpdatedAt(a.updatedAt)}`);
|
|
1158
|
-
|
|
1159
|
-
console.log(
|
|
1160
|
-
` ${chalk3.cyan.underline(`${PLAYGROUND_BASE}/${a.sessionId}`)}`
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1380
|
+
console.log(` ${chalk3.cyan.underline(artifactUrlFor(a.sessionId))}`);
|
|
1163
1381
|
console.log();
|
|
1164
1382
|
}
|
|
1165
1383
|
if (result.truncated) {
|
|
@@ -1172,8 +1390,9 @@ var listCommand = new Command2("list").aliases(["ls", "artifacts"]).description(
|
|
|
1172
1390
|
} else {
|
|
1173
1391
|
outputResult(mode, {
|
|
1174
1392
|
success: true,
|
|
1175
|
-
|
|
1176
|
-
|
|
1393
|
+
// The listing tool returns no URL; deriving it here saves every caller
|
|
1394
|
+
// the same string concatenation, and keeps one definition of the link.
|
|
1395
|
+
artifacts: artifacts.map((a) => ({ ...a, artifactUrl: artifactUrlFor(a.sessionId) })),
|
|
1177
1396
|
truncated: result.truncated ?? false
|
|
1178
1397
|
});
|
|
1179
1398
|
}
|
|
@@ -1197,9 +1416,135 @@ function formatUpdatedAt(iso) {
|
|
|
1197
1416
|
return `${relative2} (${iso})`;
|
|
1198
1417
|
}
|
|
1199
1418
|
|
|
1419
|
+
// src/commands/workspaces.ts
|
|
1420
|
+
import { Command as Command4 } from "commander";
|
|
1421
|
+
import chalk4 from "chalk";
|
|
1422
|
+
var workspacesCommand = new Command4("workspaces").summary("List the workspaces you can reach, and what you may do in each").aliases(["ws"]).description(
|
|
1423
|
+
"List the workspaces you can read in your team, with the capabilities you hold in each. Use a workspace id with `list --workspace` or `create --workspace`."
|
|
1424
|
+
).option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
|
|
1425
|
+
const mode = detectOutputMode(opts);
|
|
1426
|
+
initLogger(opts.logFile);
|
|
1427
|
+
try {
|
|
1428
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
1429
|
+
const result = await withMcp(
|
|
1430
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1431
|
+
(client) => client.callTool("workspace-list_workspaces", {})
|
|
1432
|
+
);
|
|
1433
|
+
if (!result.success) {
|
|
1434
|
+
const message = result.message || result.reason || result.error || "Could not list workspaces";
|
|
1435
|
+
throw apiError(message, result);
|
|
1436
|
+
}
|
|
1437
|
+
const workspaces = result.workspaces ?? [];
|
|
1438
|
+
logEvent("workspaces_listed", { count: workspaces.length });
|
|
1439
|
+
if (mode === "pretty") {
|
|
1440
|
+
console.log();
|
|
1441
|
+
if (workspaces.length === 0) {
|
|
1442
|
+
console.log(" You cannot reach any workspace in this team.");
|
|
1443
|
+
console.log(
|
|
1444
|
+
chalk4.gray(" Someone who manages the team can grant you access to one.")
|
|
1445
|
+
);
|
|
1446
|
+
console.log();
|
|
1447
|
+
} else {
|
|
1448
|
+
for (const workspace of workspaces) {
|
|
1449
|
+
const label = workspace.isGeneral && workspace.name !== "General" ? chalk4.gray(" (General)") : "";
|
|
1450
|
+
console.log(` ${chalk4.bold(workspace.name)}${label}`);
|
|
1451
|
+
console.log(` ${chalk4.gray("id:")} ${workspace.workspaceId}`);
|
|
1452
|
+
console.log(
|
|
1453
|
+
` ${chalk4.gray("you can:")} ${workspace.capabilities.join(", ") || chalk4.gray("nothing")}`
|
|
1454
|
+
);
|
|
1455
|
+
console.log();
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
outputResult(mode, { success: true, workspaces });
|
|
1460
|
+
}
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
if (err instanceof CliError) {
|
|
1463
|
+
if (mode === "pretty") {
|
|
1464
|
+
console.error(err.message);
|
|
1465
|
+
} else {
|
|
1466
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1467
|
+
}
|
|
1468
|
+
process.exit(err.exitCode);
|
|
1469
|
+
}
|
|
1470
|
+
throw err;
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// src/commands/status.ts
|
|
1475
|
+
import { Command as Command5 } from "commander";
|
|
1476
|
+
import chalk5 from "chalk";
|
|
1477
|
+
var statusCommand = new Command5("status").summary("Check whether an artifact finished generating").description(
|
|
1478
|
+
"Check whether an artifact finished generating. --wait blocks until it is ready or failed."
|
|
1479
|
+
).argument("<artifactUrl>", "Artifact URL or session ID").option("--wait", "Block until generation finishes instead of returning a snapshot").option("--json", "Output as JSON").option("--timeout <ms>", "How long --wait may block", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
|
|
1480
|
+
const mode = detectOutputMode(opts);
|
|
1481
|
+
initLogger(opts.logFile);
|
|
1482
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
1483
|
+
try {
|
|
1484
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
1485
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
1486
|
+
progress.start(opts.wait ? "Waiting for generation..." : "Checking status...");
|
|
1487
|
+
const result = await withMcp(
|
|
1488
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1489
|
+
(client) => opts.wait ? waitForArtifact(client, sessionId, {
|
|
1490
|
+
deadline: Date.now() + parseInt(opts.timeout, 10),
|
|
1491
|
+
onProgress: (message) => progress.update(message)
|
|
1492
|
+
}) : fetchArtifactStatus(client, sessionId)
|
|
1493
|
+
);
|
|
1494
|
+
if (!result.success) {
|
|
1495
|
+
throw apiError(result.reason || result.error || "Could not read the artifact's status", result);
|
|
1496
|
+
}
|
|
1497
|
+
progress.succeed(`Status: ${result.status}`);
|
|
1498
|
+
if (mode === "pretty") {
|
|
1499
|
+
printArtifactStatus(sessionId, result);
|
|
1500
|
+
} else {
|
|
1501
|
+
outputResult(mode, { ...result, sessionId: result.sessionId ?? sessionId });
|
|
1502
|
+
}
|
|
1503
|
+
if (result.status === "failed") {
|
|
1504
|
+
process.exit(1);
|
|
1505
|
+
}
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
progress.fail("Failed");
|
|
1508
|
+
if (err instanceof CliError) {
|
|
1509
|
+
if (mode === "pretty") {
|
|
1510
|
+
printError(err.message);
|
|
1511
|
+
} else {
|
|
1512
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1513
|
+
}
|
|
1514
|
+
process.exit(err.exitCode);
|
|
1515
|
+
}
|
|
1516
|
+
throw err;
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
function printArtifactStatus(sessionId, result) {
|
|
1520
|
+
console.log();
|
|
1521
|
+
if (result.status === "ready") {
|
|
1522
|
+
printSuccess("Ready!");
|
|
1523
|
+
} else if (result.status === "failed") {
|
|
1524
|
+
printError(`Generation failed: ${result.error ?? "no reason given"}`);
|
|
1525
|
+
}
|
|
1526
|
+
printKeyValue("Session ID", result.sessionId ?? sessionId);
|
|
1527
|
+
printKeyValue("Status", result.status ?? "unknown");
|
|
1528
|
+
if (result.name) printKeyValue("Name", result.name);
|
|
1529
|
+
if (result.status === "generating" && result.progress !== void 0) {
|
|
1530
|
+
printKeyValue("Progress", `${result.progress}%`);
|
|
1531
|
+
}
|
|
1532
|
+
if (result.artifactUrl) {
|
|
1533
|
+
printKeyValue("Your artifact", chalk5.cyan.underline(result.artifactUrl));
|
|
1534
|
+
}
|
|
1535
|
+
if (result.playgroundUrl) {
|
|
1536
|
+
printKeyValue("Edit in Playground", chalk5.cyan.underline(result.playgroundUrl));
|
|
1537
|
+
}
|
|
1538
|
+
if (result.nextStep) {
|
|
1539
|
+
console.log();
|
|
1540
|
+
console.log(` Still generating \u2014 run this again with --wait to keep waiting.`);
|
|
1541
|
+
}
|
|
1542
|
+
console.log();
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1200
1545
|
// src/commands/publish.ts
|
|
1201
|
-
import { Command as
|
|
1202
|
-
var publishCommand = new
|
|
1546
|
+
import { Command as Command6 } from "commander";
|
|
1547
|
+
var publishCommand = new Command6("publish").summary("Deploy to a public live URL (only when asked)").description("Deploy an artifact's app to a live public URL (1-3 min)").argument("<sessionId>", "Session ID from create command").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
|
|
1203
1548
|
const mode = detectOutputMode(opts);
|
|
1204
1549
|
initLogger(opts.logFile);
|
|
1205
1550
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -1245,7 +1590,7 @@ var publishCommand = new Command3("publish").description("Deploy an artifact's a
|
|
|
1245
1590
|
});
|
|
1246
1591
|
|
|
1247
1592
|
// src/commands/codegen.ts
|
|
1248
|
-
import { Command as
|
|
1593
|
+
import { Command as Command7 } from "commander";
|
|
1249
1594
|
|
|
1250
1595
|
// src/utils/files.ts
|
|
1251
1596
|
import fs4 from "fs";
|
|
@@ -1273,7 +1618,7 @@ function writeFilesToDisk(files, outputDir) {
|
|
|
1273
1618
|
}
|
|
1274
1619
|
|
|
1275
1620
|
// src/commands/codegen.ts
|
|
1276
|
-
var codegenCommand = new
|
|
1621
|
+
var codegenCommand = new Command7("codegen").summary("Convert a Figma design to local code files").description("Convert Figma design to code files (no artifact)").requiredOption("--file-key <key>", "Figma file key or URL").option("--nodes <nodes>", "Figma node IDs, comma-separated").option("--figma-token <token>", "Figma personal access token (or set FIGMA_TOKEN env var)").option("-o, --output <dir>", "Output directory", "./generated").option("--framework <fw>", "react | html", "react").option("--styling <style>", "tailwind | plain_css", "tailwind").option("--language <lang>", "typescript | javascript").option("--ui-library <lib>", "mui | antd | shadcn | clean_react").option("--json", "Output as JSON").option("--timeout <ms>", "Timeout in milliseconds", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
|
|
1277
1622
|
const mode = detectOutputMode(opts);
|
|
1278
1623
|
initLogger(opts.logFile);
|
|
1279
1624
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -1345,8 +1690,8 @@ var codegenCommand = new Command4("codegen").description("Convert Figma design t
|
|
|
1345
1690
|
});
|
|
1346
1691
|
|
|
1347
1692
|
// src/commands/getGitToken.ts
|
|
1348
|
-
import { Command as
|
|
1349
|
-
import
|
|
1693
|
+
import { Command as Command8 } from "commander";
|
|
1694
|
+
import chalk6 from "chalk";
|
|
1350
1695
|
|
|
1351
1696
|
// src/utils/gitToken.ts
|
|
1352
1697
|
function preview(payload) {
|
|
@@ -1375,17 +1720,29 @@ function parseGitTokenResponse(payload) {
|
|
|
1375
1720
|
return {
|
|
1376
1721
|
gitRemoteUrl: body.gitRemoteUrl,
|
|
1377
1722
|
access: body.access === "rw" ? "rw" : "ro",
|
|
1378
|
-
expiresAt: body.expiresAt
|
|
1723
|
+
expiresAt: body.expiresAt,
|
|
1724
|
+
...commitAuthorOf(body.commitAuthor) ? { commitAuthor: commitAuthorOf(body.commitAuthor) } : {}
|
|
1379
1725
|
};
|
|
1380
1726
|
}
|
|
1727
|
+
function commitAuthorOf(value) {
|
|
1728
|
+
if (!value || typeof value.name !== "string" || typeof value.email !== "string") return void 0;
|
|
1729
|
+
if (!value.name || !value.email) return void 0;
|
|
1730
|
+
return { name: value.name, email: value.email };
|
|
1731
|
+
}
|
|
1732
|
+
function shellQuote(value) {
|
|
1733
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
1734
|
+
}
|
|
1735
|
+
function gitConfigCommands(commitAuthor) {
|
|
1736
|
+
return `git config user.name ${shellQuote(commitAuthor.name)} && git config user.email ${shellQuote(commitAuthor.email)}`;
|
|
1737
|
+
}
|
|
1381
1738
|
|
|
1382
1739
|
// src/commands/getGitToken.ts
|
|
1383
|
-
var getGitTokenCommand = new
|
|
1740
|
+
var getGitTokenCommand = new Command8("get-git-token").summary("Mint git access to an artifact's repository").description("Mint a short-lived git remote URL for an artifact (clone/push with git)").argument("<artifactUrl>", "Artifact URL or session ID").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
|
|
1384
1741
|
const mode = detectOutputMode(opts);
|
|
1385
1742
|
initLogger(opts.logFile);
|
|
1386
1743
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
1387
1744
|
try {
|
|
1388
|
-
const sessionId =
|
|
1745
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
1389
1746
|
const auth = await resolveAuth(opts.apiUrl);
|
|
1390
1747
|
progress.start("Requesting git access...");
|
|
1391
1748
|
const response = await withMcp(
|
|
@@ -1397,9 +1754,14 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1397
1754
|
progress.succeed(`Git access granted (${grant.access === "rw" ? "read-write" : "read-only"})`);
|
|
1398
1755
|
if (mode === "pretty") {
|
|
1399
1756
|
console.log();
|
|
1400
|
-
console.log(` ${
|
|
1757
|
+
console.log(` ${chalk6.cyan(`git clone ${grant.gitRemoteUrl}`)}`);
|
|
1401
1758
|
console.log();
|
|
1402
1759
|
if (grant.access === "rw") {
|
|
1760
|
+
if (grant.commitAuthor) {
|
|
1761
|
+
console.log(" In the clone, set who your commits are from:");
|
|
1762
|
+
console.log(` ${chalk6.cyan(gitConfigCommands(grant.commitAuthor))}`);
|
|
1763
|
+
console.log();
|
|
1764
|
+
}
|
|
1403
1765
|
console.log(" Commit and push to update the live artifact.");
|
|
1404
1766
|
} else {
|
|
1405
1767
|
console.log(" Read-only: cloning works, pushing will be rejected.");
|
|
@@ -1407,7 +1769,7 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1407
1769
|
if (grant.expiresAt) {
|
|
1408
1770
|
console.log(
|
|
1409
1771
|
` The URL embeds a token that expires at ${grant.expiresAt} and cannot be renewed;
|
|
1410
|
-
re-run this command and ${
|
|
1772
|
+
re-run this command and ${chalk6.cyan("git remote set-url origin <new url>")} to keep pushing.`
|
|
1411
1773
|
);
|
|
1412
1774
|
}
|
|
1413
1775
|
console.log(" Treat the URL as a secret \u2014 it grants access to this artifact.");
|
|
@@ -1418,7 +1780,11 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1418
1780
|
sessionId,
|
|
1419
1781
|
gitRemoteUrl: grant.gitRemoteUrl,
|
|
1420
1782
|
access: grant.access,
|
|
1421
|
-
expiresAt: grant.expiresAt
|
|
1783
|
+
expiresAt: grant.expiresAt,
|
|
1784
|
+
...grant.commitAuthor ? {
|
|
1785
|
+
commitAuthor: grant.commitAuthor,
|
|
1786
|
+
gitConfigCommand: gitConfigCommands(grant.commitAuthor)
|
|
1787
|
+
} : {}
|
|
1422
1788
|
});
|
|
1423
1789
|
}
|
|
1424
1790
|
} catch (err) {
|
|
@@ -1436,11 +1802,11 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1436
1802
|
});
|
|
1437
1803
|
|
|
1438
1804
|
// src/commands/mcpConfig.ts
|
|
1439
|
-
import { Command as
|
|
1805
|
+
import { Command as Command10 } from "commander";
|
|
1440
1806
|
|
|
1441
1807
|
// src/commands/login.ts
|
|
1442
|
-
import { Command as
|
|
1443
|
-
import
|
|
1808
|
+
import { Command as Command9 } from "commander";
|
|
1809
|
+
import chalk7 from "chalk";
|
|
1444
1810
|
import open from "open";
|
|
1445
1811
|
|
|
1446
1812
|
// src/auth/device.ts
|
|
@@ -1630,7 +1996,7 @@ function resolveDeadline(claimableUntil, now) {
|
|
|
1630
1996
|
}
|
|
1631
1997
|
|
|
1632
1998
|
// src/commands/login.ts
|
|
1633
|
-
var loginCommand = new
|
|
1999
|
+
var loginCommand = new Command9("login").summary("Connect this machine as a scoped agent identity").description("Log in via the device flow, redeem an invite with --invite, or finish an anonymous create with --handoff").option("--invite <url-or-code>", "Redeem an invite link (\u2026/invite/<code>.md) or bare code \u2014 no approval step").option(
|
|
1634
2000
|
"--handoff [token]",
|
|
1635
2001
|
"Log in with the handoff token from an anonymous create. Waits until a human claims the artifact. Defaults to your last one"
|
|
1636
2002
|
).option("--client-name <name>", "How this CLI appears on the consent screen", "AgentGrid CLI").option("--no-open", "Do not try to open the verification page in a browser").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").option(
|
|
@@ -1700,9 +2066,9 @@ var loginCommand = new Command6("login").description("Log in via the device flow
|
|
|
1700
2066
|
console.log();
|
|
1701
2067
|
printInfo("To connect, open the verification page and enter this code:");
|
|
1702
2068
|
console.log();
|
|
1703
|
-
console.log(` ${
|
|
2069
|
+
console.log(` ${chalk7.cyan.underline(info.verificationUri)}`);
|
|
1704
2070
|
console.log();
|
|
1705
|
-
console.log(` ${
|
|
2071
|
+
console.log(` ${chalk7.bold.white(info.userCode)}`);
|
|
1706
2072
|
console.log();
|
|
1707
2073
|
printInfo(`Waiting for approval\u2026 (code expires in ${Math.round(info.expiresInSeconds / 60)} minutes)`);
|
|
1708
2074
|
} else {
|
|
@@ -1779,7 +2145,7 @@ async function runHandoffLogin(opts, mode) {
|
|
|
1779
2145
|
printInfo("Waiting for a human to claim the artifact:");
|
|
1780
2146
|
if (pending.artifactUrl) {
|
|
1781
2147
|
console.log();
|
|
1782
|
-
console.log(` ${
|
|
2148
|
+
console.log(` ${chalk7.cyan.underline(pending.artifactUrl)}`);
|
|
1783
2149
|
}
|
|
1784
2150
|
console.log();
|
|
1785
2151
|
printInfo(
|
|
@@ -1854,7 +2220,7 @@ function buildMcpConfig(apiUrl) {
|
|
|
1854
2220
|
}
|
|
1855
2221
|
|
|
1856
2222
|
// src/commands/mcpConfig.ts
|
|
1857
|
-
var mcpConfigCommand = new
|
|
2223
|
+
var mcpConfigCommand = new Command10("mcp-config").summary("Print an MCP server config for your MCP client").description("Print an MCP server config for your MCP client").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").action(async (opts) => {
|
|
1858
2224
|
const mode = detectOutputMode(opts);
|
|
1859
2225
|
try {
|
|
1860
2226
|
const mcpConfig = buildMcpConfig(resolveApiUrl(opts.apiUrl));
|
|
@@ -1881,8 +2247,8 @@ var mcpConfigCommand = new Command7("mcp-config").description("Print an MCP serv
|
|
|
1881
2247
|
});
|
|
1882
2248
|
|
|
1883
2249
|
// src/commands/auth.ts
|
|
1884
|
-
import { Command as
|
|
1885
|
-
import
|
|
2250
|
+
import { Command as Command11 } from "commander";
|
|
2251
|
+
import chalk8 from "chalk";
|
|
1886
2252
|
var DEFAULT_API_URL2 = "https://api.agentgrid.io";
|
|
1887
2253
|
function describePendingHandoff(pending, state) {
|
|
1888
2254
|
return {
|
|
@@ -1900,10 +2266,10 @@ function printPendingHandoff(pending, state) {
|
|
|
1900
2266
|
return;
|
|
1901
2267
|
}
|
|
1902
2268
|
printInfo(`Anonymous artifact ${pending.sessionId} is awaiting a claim (until ${pending.claimableUntil}):`);
|
|
1903
|
-
console.log(` ${
|
|
1904
|
-
console.log(` Run ${
|
|
2269
|
+
console.log(` ${chalk8.cyan.underline(pending.artifactUrl)}`);
|
|
2270
|
+
console.log(` Run ${chalk8.cyan("anima login --handoff")} to wait for it.`);
|
|
1905
2271
|
}
|
|
1906
|
-
var authCommand = new
|
|
2272
|
+
var authCommand = new Command11("auth").summary("Inspect or manage stored credentials").description("Manage authentication").option("--token <token>", "Store AgentGrid API token").option("--figma-token <token>", "Store Figma personal access token").option("--team-id <teamId>", "Team ID to associate with token").option("--status", "Show current auth status").option("--logout", "Clear stored credentials (alias for `anima logout`)").option("--json", "Output as JSON").option("--api-url <url>", "API base URL", DEFAULT_API_URL2).action(async (opts) => {
|
|
1907
2273
|
const mode = detectOutputMode(opts);
|
|
1908
2274
|
try {
|
|
1909
2275
|
if (opts.token || opts.figmaToken) {
|
|
@@ -2003,13 +2369,13 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
|
|
|
2003
2369
|
if (mode === "pretty") {
|
|
2004
2370
|
console.log();
|
|
2005
2371
|
printInfo("Auth commands:");
|
|
2006
|
-
console.log(` ${
|
|
2007
|
-
console.log(` ${
|
|
2008
|
-
console.log(` ${
|
|
2009
|
-
console.log(` ${
|
|
2010
|
-
console.log(` ${
|
|
2372
|
+
console.log(` ${chalk8.cyan("anima login")} Log in (device flow \u2014 approve on any browser)`);
|
|
2373
|
+
console.log(` ${chalk8.cyan("anima login --handoff")} Wait for a human to claim your anonymous artifact`);
|
|
2374
|
+
console.log(` ${chalk8.cyan("anima logout")} Clear all credentials`);
|
|
2375
|
+
console.log(` ${chalk8.cyan("anima auth --status")} Check auth status`);
|
|
2376
|
+
console.log(` ${chalk8.cyan("anima auth --figma-token <T>")} Save Figma token (needed for codegen/f2c)`);
|
|
2011
2377
|
console.log();
|
|
2012
|
-
console.log(` Figma token: ${
|
|
2378
|
+
console.log(` Figma token: ${chalk8.cyan.underline("https://www.figma.com")} > Settings > Personal access tokens`);
|
|
2013
2379
|
console.log();
|
|
2014
2380
|
} else {
|
|
2015
2381
|
outputResult(mode, {
|
|
@@ -2031,8 +2397,8 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
|
|
|
2031
2397
|
});
|
|
2032
2398
|
|
|
2033
2399
|
// src/commands/logout.ts
|
|
2034
|
-
import { Command as
|
|
2035
|
-
var logoutCommand = new
|
|
2400
|
+
import { Command as Command12 } from "commander";
|
|
2401
|
+
var logoutCommand = new Command12("logout").summary("Disconnect: revoke and clear every stored credential").description("Clear all stored credentials (AgentGrid + Figma tokens) from this machine").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").action(async (opts) => {
|
|
2036
2402
|
const mode = detectOutputMode(opts);
|
|
2037
2403
|
const revoked = await revokeStoredRefreshToken(resolveApiUrl(opts.apiUrl));
|
|
2038
2404
|
const hadCredentials = clearStorage();
|
|
@@ -2050,8 +2416,8 @@ var logoutCommand = new Command9("logout").description("Clear all stored credent
|
|
|
2050
2416
|
});
|
|
2051
2417
|
|
|
2052
2418
|
// src/commands/unpublish.ts
|
|
2053
|
-
import { Command as
|
|
2054
|
-
var unpublishCommand = new
|
|
2419
|
+
import { Command as Command13 } from "commander";
|
|
2420
|
+
var unpublishCommand = new Command13("unpublish").summary("Take a published artifact offline").description("Take a published artifact offline (clears its live URL; code and artifact stay intact)").argument("<sessionId>", "Session ID of the published artifact").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
|
|
2055
2421
|
const mode = detectOutputMode(opts);
|
|
2056
2422
|
initLogger(opts.logFile);
|
|
2057
2423
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -2086,8 +2452,8 @@ var unpublishCommand = new Command10("unpublish").description("Take a published
|
|
|
2086
2452
|
});
|
|
2087
2453
|
|
|
2088
2454
|
// src/commands/update.ts
|
|
2089
|
-
import { Command as
|
|
2090
|
-
var updateCommand = new
|
|
2455
|
+
import { Command as Command14 } from "commander";
|
|
2456
|
+
var updateCommand = new Command14("update").summary("Rename an artifact or change its visibility").description("Update an artifact's metadata: display name and/or visibility (never touches code)").argument("<sessionId>", "Session ID of the artifact").option("--name <name>", "New display name").option("--privacy <privacy>", "public (anyone with the link) | private (team only)").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
|
|
2091
2457
|
const mode = detectOutputMode(opts);
|
|
2092
2458
|
initLogger(opts.logFile);
|
|
2093
2459
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -2132,8 +2498,8 @@ var updateCommand = new Command11("update").description("Update an artifact's me
|
|
|
2132
2498
|
});
|
|
2133
2499
|
|
|
2134
2500
|
// src/commands/config.ts
|
|
2135
|
-
import { Command as
|
|
2136
|
-
var configCommand = new
|
|
2501
|
+
import { Command as Command15 } from "commander";
|
|
2502
|
+
var configCommand = new Command15("config").summary("Store CLI preferences (e.g. a default API URL)").description(
|
|
2137
2503
|
`Store CLI preferences (keys: ${configKeys().join(", ")})`
|
|
2138
2504
|
);
|
|
2139
2505
|
configCommand.command("set <key> <value>").description("Set a config value").option("--json", "Output as JSON").action((key, value, opts) => {
|
|
@@ -2199,27 +2565,34 @@ function assertUrl(value) {
|
|
|
2199
2565
|
}
|
|
2200
2566
|
|
|
2201
2567
|
// src/commands/duplicate.ts
|
|
2202
|
-
import { Command as
|
|
2203
|
-
import
|
|
2204
|
-
function buildDuplicateArgs(artifactUrl, name) {
|
|
2205
|
-
const sessionId =
|
|
2568
|
+
import { Command as Command16 } from "commander";
|
|
2569
|
+
import chalk9 from "chalk";
|
|
2570
|
+
function buildDuplicateArgs(artifactUrl, name, workspaceId) {
|
|
2571
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
2206
2572
|
if (!sessionId) {
|
|
2207
2573
|
throw validationError("A source artifact URL or session ID is required");
|
|
2208
2574
|
}
|
|
2209
2575
|
if (name !== void 0 && (name.length === 0 || name.length > 120)) {
|
|
2210
2576
|
throw validationError("--name must be between 1 and 120 characters");
|
|
2211
2577
|
}
|
|
2212
|
-
return {
|
|
2578
|
+
return {
|
|
2579
|
+
sessionId,
|
|
2580
|
+
...name !== void 0 ? { name } : {},
|
|
2581
|
+
...workspaceFlag(workspaceId) !== void 0 ? { workspaceId } : {}
|
|
2582
|
+
};
|
|
2213
2583
|
}
|
|
2214
2584
|
function createDuplicateCommand() {
|
|
2215
|
-
return new
|
|
2585
|
+
return new Command16("duplicate").summary("Copy an artifact into a new, independent one").description(
|
|
2216
2586
|
"Duplicate an artifact into a new, independent artifact. Copies code, assets, and supported database content; chat and custom domains are not copied. Duplication is non-idempotent: if a request times out or its response is lost, list recent artifacts before retrying because the first request may already have created the copy."
|
|
2217
|
-
).argument("<artifactUrl>", "Source artifact URL or session ID").option("--name <name>", "Display name for the duplicate (defaults to '<source name> (Copy)')").option(
|
|
2587
|
+
).argument("<artifactUrl>", "Source artifact URL or session ID").option("--name <name>", "Display name for the duplicate (defaults to '<source name> (Copy)')").option(
|
|
2588
|
+
"-w, --workspace <id>",
|
|
2589
|
+
"The workspace to put the copy in (id from `anima workspaces`). Optional when you can create in only one workspace; otherwise required, since there is no default"
|
|
2590
|
+
).option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
|
|
2218
2591
|
const mode = detectOutputMode(opts);
|
|
2219
2592
|
initLogger(opts.logFile);
|
|
2220
2593
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
2221
2594
|
try {
|
|
2222
|
-
const args = buildDuplicateArgs(artifactUrl, opts.name);
|
|
2595
|
+
const args = buildDuplicateArgs(artifactUrl, opts.name, opts.workspace);
|
|
2223
2596
|
const auth = await resolveAuth(opts.apiUrl);
|
|
2224
2597
|
progress.start("Duplicating artifact...");
|
|
2225
2598
|
const result = await withMcp(
|
|
@@ -2227,7 +2600,7 @@ function createDuplicateCommand() {
|
|
|
2227
2600
|
(client) => client.callTool("artifact-duplicate", args)
|
|
2228
2601
|
);
|
|
2229
2602
|
if (!result.success) {
|
|
2230
|
-
throw apiError(result.reason || result.error || "Artifact duplication failed", result);
|
|
2603
|
+
throw apiError(result.message || result.reason || result.error || "Artifact duplication failed", result);
|
|
2231
2604
|
}
|
|
2232
2605
|
progress.succeed("Artifact duplicated");
|
|
2233
2606
|
if (mode === "pretty") {
|
|
@@ -2236,8 +2609,13 @@ function createDuplicateCommand() {
|
|
|
2236
2609
|
printKeyValue("Name", result.name);
|
|
2237
2610
|
printKeyValue("Session ID", result.sessionId);
|
|
2238
2611
|
printKeyValue("Source session", result.sourceSessionId);
|
|
2239
|
-
printKeyValue("Artifact",
|
|
2240
|
-
|
|
2612
|
+
printKeyValue("Artifact", chalk9.cyan.underline(result.artifactUrl));
|
|
2613
|
+
if (result.playgroundUrl) {
|
|
2614
|
+
printKeyValue("Edit in Playground", chalk9.cyan.underline(result.playgroundUrl));
|
|
2615
|
+
}
|
|
2616
|
+
if (result.previewUrl) {
|
|
2617
|
+
printKeyValue("Preview", chalk9.cyan.underline(result.previewUrl));
|
|
2618
|
+
}
|
|
2241
2619
|
printKeyValue("Next steps", result.nextSteps);
|
|
2242
2620
|
console.log();
|
|
2243
2621
|
} else {
|
|
@@ -2259,70 +2637,808 @@ function createDuplicateCommand() {
|
|
|
2259
2637
|
}
|
|
2260
2638
|
var duplicateCommand = createDuplicateCommand();
|
|
2261
2639
|
|
|
2640
|
+
// src/commands/explore.ts
|
|
2641
|
+
import { Command as Command17 } from "commander";
|
|
2642
|
+
import chalk10 from "chalk";
|
|
2643
|
+
|
|
2644
|
+
// src/api/tool.ts
|
|
2645
|
+
async function runTool(toolName, args, options = {}) {
|
|
2646
|
+
const auth = await resolveAuth(options.apiUrl);
|
|
2647
|
+
const result = await withMcp(
|
|
2648
|
+
{
|
|
2649
|
+
apiBaseUrl: resolveApiUrl(options.apiUrl),
|
|
2650
|
+
token: auth.token,
|
|
2651
|
+
teamId: auth.teamId,
|
|
2652
|
+
...options.figmaToken ? { figmaToken: options.figmaToken } : {}
|
|
2653
|
+
},
|
|
2654
|
+
(client) => client.callTool(toolName, args, options.call)
|
|
2655
|
+
);
|
|
2656
|
+
if (result.success === false) {
|
|
2657
|
+
throw apiError(describeFailure(result), result);
|
|
2658
|
+
}
|
|
2659
|
+
return result;
|
|
2660
|
+
}
|
|
2661
|
+
function describeFailure(result) {
|
|
2662
|
+
const base = typeof result.error === "object" && result.error ? result.error.message || result.error.code || "The tool reported a failure" : result.reason || result.error || "The tool reported a failure";
|
|
2663
|
+
const nextSteps = Array.isArray(result.nextSteps) ? result.nextSteps.join("\n") : result.nextSteps;
|
|
2664
|
+
return nextSteps ? `${base}
|
|
2665
|
+
${nextSteps}` : base;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/commands/explore.ts
|
|
2669
|
+
var exploreCommand = new Command17("explore").summary("Read an artifact's files: list, search, read, history").description(
|
|
2670
|
+
"Inspect an artifact's files without git: list them (--tree), search them (--search), read them (--read), or show commits (--history)."
|
|
2671
|
+
).argument("<artifactUrl>", "Artifact URL or session ID").option("--tree", "List file paths").option("--search <query>", "Find text inside files \u2014 start here when you don't know the file").option("--read <paths...>", "Return the contents of these files").option("--history", "List commits, newest first").option("--path <prefix>", "tree/search: a literal directory prefix (not a glob)").option("--range <start,end>", "read: line window, 1-based inclusive, for a single file").option("--revision <rev>", "Look at the artifact as it was at this commit").option("--regex", "search: treat the query as a JavaScript regular expression").option("--case-sensitive", "search: match case exactly").option("--include-excluded", "tree/search: also look inside node_modules, dist and build").option("--limit <n>", "tree/search/history: maximum rows to return").option("--cursor <cursor>", "history: continue after a previous response's nextCursor").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
|
|
2672
|
+
const mode = detectOutputMode(opts);
|
|
2673
|
+
initLogger(opts.logFile);
|
|
2674
|
+
await withCommandErrors(mode, async () => {
|
|
2675
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
2676
|
+
const args = buildExploreArgs(sessionId, opts);
|
|
2677
|
+
const result = await runTool("artifact-explore", args, { apiUrl: opts.apiUrl });
|
|
2678
|
+
if (mode === "pretty") {
|
|
2679
|
+
printExplore(args.action, result);
|
|
2680
|
+
} else {
|
|
2681
|
+
outputResult(mode, result);
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
});
|
|
2685
|
+
function buildExploreArgs(sessionId, opts) {
|
|
2686
|
+
const chosen = [
|
|
2687
|
+
opts.tree ? "tree" : null,
|
|
2688
|
+
opts.search ? "search" : null,
|
|
2689
|
+
opts.read ? "read" : null,
|
|
2690
|
+
opts.history ? "history" : null
|
|
2691
|
+
].filter(Boolean);
|
|
2692
|
+
if (chosen.length !== 1) {
|
|
2693
|
+
throw validationError(
|
|
2694
|
+
"Pass exactly one of --tree, --search <query>, --read <paths...> or --history. Not sure which file to change? Start with --search."
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
const action = chosen[0];
|
|
2698
|
+
const args = { sessionId, action };
|
|
2699
|
+
if (opts.revision) args.revision = opts.revision;
|
|
2700
|
+
if (opts.limit !== void 0) args.limit = parseLimit(opts.limit);
|
|
2701
|
+
switch (action) {
|
|
2702
|
+
case "tree":
|
|
2703
|
+
if (opts.path) args.path = opts.path;
|
|
2704
|
+
if (opts.includeExcluded) args.includeExcluded = true;
|
|
2705
|
+
break;
|
|
2706
|
+
case "search":
|
|
2707
|
+
args.query = opts.search;
|
|
2708
|
+
if (opts.path) args.path = opts.path;
|
|
2709
|
+
if (opts.regex) args.regex = true;
|
|
2710
|
+
if (opts.caseSensitive) args.caseSensitive = true;
|
|
2711
|
+
if (opts.includeExcluded) args.includeExcluded = true;
|
|
2712
|
+
break;
|
|
2713
|
+
case "read": {
|
|
2714
|
+
const paths = opts.read;
|
|
2715
|
+
args.paths = paths;
|
|
2716
|
+
if (opts.range) {
|
|
2717
|
+
if (paths.length !== 1) {
|
|
2718
|
+
throw validationError("--range applies to a single file; pass one path to --read.");
|
|
2719
|
+
}
|
|
2720
|
+
args.range = opts.range;
|
|
2721
|
+
}
|
|
2722
|
+
break;
|
|
2723
|
+
}
|
|
2724
|
+
case "history":
|
|
2725
|
+
if (opts.cursor) args.cursor = opts.cursor;
|
|
2726
|
+
break;
|
|
2727
|
+
}
|
|
2728
|
+
return args;
|
|
2729
|
+
}
|
|
2730
|
+
function parseLimit(value) {
|
|
2731
|
+
const limit = Number(value);
|
|
2732
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
2733
|
+
throw validationError(`--limit must be a positive whole number (got "${value}").`);
|
|
2734
|
+
}
|
|
2735
|
+
return limit;
|
|
2736
|
+
}
|
|
2737
|
+
function printExplore(action, result) {
|
|
2738
|
+
console.log();
|
|
2739
|
+
if (action === "tree") {
|
|
2740
|
+
for (const file of result.files ?? []) {
|
|
2741
|
+
const kind = result.unwritable?.[file];
|
|
2742
|
+
console.log(` ${file}${kind ? chalk10.gray(` (${kind} \u2014 git only)`) : ""}`);
|
|
2743
|
+
}
|
|
2744
|
+
if (result.matchedFiles !== void 0) {
|
|
2745
|
+
console.log();
|
|
2746
|
+
console.log(chalk10.gray(` ${(result.files ?? []).length} of ${result.matchedFiles} shown`));
|
|
2747
|
+
}
|
|
2748
|
+
} else if (action === "search") {
|
|
2749
|
+
for (const match of result.matches ?? []) {
|
|
2750
|
+
console.log(` ${chalk10.cyan(match.path)}:${match.line} ${match.lineText.trim()}`);
|
|
2751
|
+
}
|
|
2752
|
+
if (!result.matches?.length) {
|
|
2753
|
+
console.log(" No matches.");
|
|
2754
|
+
const skipped = result.notSearched;
|
|
2755
|
+
if (skipped?.excluded) {
|
|
2756
|
+
console.log(
|
|
2757
|
+
chalk10.yellow(
|
|
2758
|
+
` ${skipped.excluded} files were skipped (node_modules, dist, build) \u2014 retry with --include-excluded.`
|
|
2759
|
+
)
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
if (skipped?.assets) {
|
|
2763
|
+
console.log(chalk10.gray(` ${skipped.assets} binary files cannot match a text query.`));
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
} else if (action === "read") {
|
|
2767
|
+
for (const file of result.files ?? []) {
|
|
2768
|
+
console.log(chalk10.bold(` ${file.path}`));
|
|
2769
|
+
if (file.asset) {
|
|
2770
|
+
console.log(chalk10.gray(` binary \u2014 ${file.size ?? "?"} bytes, ${file.mime ?? "unknown type"}`));
|
|
2771
|
+
} else if (file.deferred) {
|
|
2772
|
+
console.log(chalk10.yellow(" not returned: the response ran out of budget \u2014 read it on its own"));
|
|
2773
|
+
} else {
|
|
2774
|
+
console.log(file.content ?? "");
|
|
2775
|
+
if (file.truncated) {
|
|
2776
|
+
console.log(
|
|
2777
|
+
chalk10.yellow(
|
|
2778
|
+
` lines ${file.startLine}-${file.endLine} of ${file.totalLines} \u2014 read the rest with --range`
|
|
2779
|
+
)
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
console.log();
|
|
2784
|
+
}
|
|
2785
|
+
for (const missing of result.missingPaths ?? []) {
|
|
2786
|
+
const near = result.suggestions?.[missing];
|
|
2787
|
+
console.log(chalk10.yellow(` Not found: ${missing}${near?.length ? ` \u2014 did you mean ${near.join(", ")}?` : ""}`));
|
|
2788
|
+
}
|
|
2789
|
+
} else {
|
|
2790
|
+
const commits = result.commit ? [result.commit] : result.commits ?? [];
|
|
2791
|
+
for (const commit of commits) {
|
|
2792
|
+
console.log(` ${chalk10.yellow(commit.revision.slice(0, 10))} ${commit.message.split("\n")[0]}`);
|
|
2793
|
+
console.log(chalk10.gray(` ${commit.author} \u2014 ${commit.date}`));
|
|
2794
|
+
}
|
|
2795
|
+
for (const changed of result.commit?.changedFiles ?? []) {
|
|
2796
|
+
console.log(chalk10.gray(` ${changed.change.padEnd(8)} ${changed.path}`));
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
if (result.truncated) {
|
|
2800
|
+
console.log(chalk10.yellow(" The list was capped \u2014 raise --limit to see more."));
|
|
2801
|
+
}
|
|
2802
|
+
if (result.revision) {
|
|
2803
|
+
console.log();
|
|
2804
|
+
console.log(chalk10.gray(` revision: ${result.revision} (pass to \`anima edit --base-revision\`)`));
|
|
2805
|
+
}
|
|
2806
|
+
console.log();
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
// src/commands/edit.ts
|
|
2810
|
+
import { Command as Command18 } from "commander";
|
|
2811
|
+
import fs5 from "fs";
|
|
2812
|
+
import chalk11 from "chalk";
|
|
2813
|
+
var editCommand = new Command18("edit").summary("Change an artifact's files and commit them").description(
|
|
2814
|
+
"Change an artifact's files and commit them, without git. All changes land as one commit."
|
|
2815
|
+
).argument("<artifactUrl>", "Artifact URL or session ID").requiredOption("-m, --message <message>", "Commit message").option(
|
|
2816
|
+
"--base-revision <rev>",
|
|
2817
|
+
"The revision the edit applies to (from `anima explore`). Defaults to the artifact's current head"
|
|
2818
|
+
).option("--changes <json>", "The full change list as a JSON array \u2014 the general door").option("--changes-file <path>", "Read that JSON array from a file").option("--write <path>", "Shorthand: create or replace this file").option("--from-file <path>", "--write: take the content from this local file (default: stdin is not read)").option("--content <text>", "--write: the file's complete new content").option("--delete <path>", "Shorthand: delete this file").option("--move <path>", "Shorthand: rename this file (needs --to)").option("--to <path>", "--move: the new path").option("--replace <path>", "Shorthand: swap an exact snippet in this file (needs --old and --new)").option("--old <text>", "--replace: the exact text to replace").option("--new <text>", "--replace: the replacement text").option("--all", "--replace: replace every occurrence instead of requiring exactly one").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
|
|
2819
|
+
const mode = detectOutputMode(opts);
|
|
2820
|
+
initLogger(opts.logFile);
|
|
2821
|
+
await withCommandErrors(mode, async () => {
|
|
2822
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
2823
|
+
const changes = buildChanges(opts);
|
|
2824
|
+
const baseRevision = opts.baseRevision ?? await currentRevision(sessionId, opts.apiUrl);
|
|
2825
|
+
const result = await runTool(
|
|
2826
|
+
"artifact-edit",
|
|
2827
|
+
{ sessionId, baseRevision, commitMessage: opts.message, changes },
|
|
2828
|
+
{ apiUrl: opts.apiUrl }
|
|
2829
|
+
);
|
|
2830
|
+
if (mode === "pretty") {
|
|
2831
|
+
console.log();
|
|
2832
|
+
printSuccess("Committed.");
|
|
2833
|
+
for (const changed of result.changedFiles ?? []) {
|
|
2834
|
+
const detail = changed.op === "move" ? `${changed.fromPath} \u2192 ${changed.path}` : changed.replacements !== void 0 ? `${changed.path} (${changed.replacements} replaced)` : changed.path;
|
|
2835
|
+
console.log(` ${chalk11.gray(changed.op.padEnd(12))} ${detail}`);
|
|
2836
|
+
}
|
|
2837
|
+
console.log();
|
|
2838
|
+
printKeyValue("Revision", result.revision ?? "unknown");
|
|
2839
|
+
console.log();
|
|
2840
|
+
console.log(" The live artifact reflects this commit.");
|
|
2841
|
+
console.log();
|
|
2842
|
+
} else {
|
|
2843
|
+
outputResult(mode, result);
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
});
|
|
2847
|
+
async function currentRevision(sessionId, apiUrl) {
|
|
2848
|
+
const result = await runTool(
|
|
2849
|
+
"artifact-explore",
|
|
2850
|
+
{ sessionId, action: "tree", limit: 1 },
|
|
2851
|
+
{ apiUrl }
|
|
2852
|
+
);
|
|
2853
|
+
if (!result.revision) {
|
|
2854
|
+
throw validationError(
|
|
2855
|
+
"Could not read the artifact's current revision. Pass --base-revision from `anima explore`."
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
return result.revision;
|
|
2859
|
+
}
|
|
2860
|
+
function buildChanges(opts) {
|
|
2861
|
+
const explicit = opts.changes ?? opts.changesFile;
|
|
2862
|
+
const shorthands = [opts.write, opts.delete, opts.move, opts.replace].filter(Boolean);
|
|
2863
|
+
if (explicit && shorthands.length) {
|
|
2864
|
+
throw validationError(
|
|
2865
|
+
"--changes/--changes-file already carries the whole change list; drop the --write/--delete/--move/--replace shorthand."
|
|
2866
|
+
);
|
|
2867
|
+
}
|
|
2868
|
+
if (explicit) {
|
|
2869
|
+
const raw = opts.changesFile ? readFile(opts.changesFile) : opts.changes;
|
|
2870
|
+
return parseChanges(raw);
|
|
2871
|
+
}
|
|
2872
|
+
if (shorthands.length !== 1) {
|
|
2873
|
+
throw validationError(
|
|
2874
|
+
"Nothing to change. Pass --changes <json> (or --changes-file), or exactly one of --write <path>, --delete <path>, --move <path> --to <path>, --replace <path> --old <text> --new <text>."
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
if (opts.write) {
|
|
2878
|
+
if (opts.content === void 0 && !opts.fromFile) {
|
|
2879
|
+
throw validationError("--write needs the file's content: pass --content <text> or --from-file <path>.");
|
|
2880
|
+
}
|
|
2881
|
+
if (opts.content !== void 0 && opts.fromFile) {
|
|
2882
|
+
throw validationError("Pass either --content or --from-file for --write, not both.");
|
|
2883
|
+
}
|
|
2884
|
+
return [
|
|
2885
|
+
{
|
|
2886
|
+
op: "write",
|
|
2887
|
+
path: opts.write,
|
|
2888
|
+
content: opts.fromFile ? readFile(opts.fromFile) : opts.content
|
|
2889
|
+
}
|
|
2890
|
+
];
|
|
2891
|
+
}
|
|
2892
|
+
if (opts.delete) {
|
|
2893
|
+
return [{ op: "delete", path: opts.delete }];
|
|
2894
|
+
}
|
|
2895
|
+
if (opts.move) {
|
|
2896
|
+
if (!opts.to) throw validationError("--move needs --to <path>, the new path.");
|
|
2897
|
+
return [{ op: "move", fromPath: opts.move, toPath: opts.to }];
|
|
2898
|
+
}
|
|
2899
|
+
if (opts.old === void 0 || opts.new === void 0) {
|
|
2900
|
+
throw validationError('--replace needs --old <text> and --new <text>. Pass --new "" to delete the snippet.');
|
|
2901
|
+
}
|
|
2902
|
+
return [
|
|
2903
|
+
{
|
|
2904
|
+
op: "str_replace",
|
|
2905
|
+
path: opts.replace,
|
|
2906
|
+
oldText: opts.old,
|
|
2907
|
+
newText: opts.new,
|
|
2908
|
+
...opts.all ? { replaceAll: true } : {}
|
|
2909
|
+
}
|
|
2910
|
+
];
|
|
2911
|
+
}
|
|
2912
|
+
function parseChanges(raw) {
|
|
2913
|
+
let parsed;
|
|
2914
|
+
try {
|
|
2915
|
+
parsed = JSON.parse(raw);
|
|
2916
|
+
} catch (err) {
|
|
2917
|
+
throw validationError(`--changes is not valid JSON: ${err.message}`);
|
|
2918
|
+
}
|
|
2919
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
2920
|
+
throw validationError("--changes must be a non-empty JSON array of change objects.");
|
|
2921
|
+
}
|
|
2922
|
+
return parsed;
|
|
2923
|
+
}
|
|
2924
|
+
function readFile(path7) {
|
|
2925
|
+
try {
|
|
2926
|
+
return fs5.readFileSync(path7, "utf-8");
|
|
2927
|
+
} catch (err) {
|
|
2928
|
+
throw validationError(`Could not read ${path7}: ${err.message}`);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// src/commands/uploadAsset.ts
|
|
2933
|
+
import { Command as Command19 } from "commander";
|
|
2934
|
+
import fs6 from "fs";
|
|
2935
|
+
import path5 from "path";
|
|
2936
|
+
import chalk12 from "chalk";
|
|
2937
|
+
var uploadAssetCommand = new Command19("upload-asset").summary("Add an image, font or media file over 256 KB").description(
|
|
2938
|
+
"Add a large image, font or media file (over 256 KB) to an artifact: stage it, upload it, and commit it at --path."
|
|
2939
|
+
).argument("<artifactUrl>", "Artifact URL or session ID").argument("<file>", "Local file to upload").option(
|
|
2940
|
+
"--path <path>",
|
|
2941
|
+
"Where the asset lands in the artifact (e.g. public/hero.png). Omit to stage only and print the assetUploadId"
|
|
2942
|
+
).option("-m, --message <message>", "Commit message for the write").option("--base-revision <rev>", "The revision the commit applies to. Defaults to the current head").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, file, opts) => {
|
|
2943
|
+
const mode = detectOutputMode(opts);
|
|
2944
|
+
initLogger(opts.logFile);
|
|
2945
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
2946
|
+
await withCommandErrors(mode, async () => {
|
|
2947
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
2948
|
+
const bytes = readAsset(file);
|
|
2949
|
+
progress.start("Staging the upload...");
|
|
2950
|
+
const grant = await runTool(
|
|
2951
|
+
"artifact-get_asset_upload_url",
|
|
2952
|
+
{ sessionId, filename: path5.basename(file) },
|
|
2953
|
+
{ apiUrl: opts.apiUrl }
|
|
2954
|
+
);
|
|
2955
|
+
if (!grant.uploadUrl || !grant.uploadFields || !grant.assetUploadId) {
|
|
2956
|
+
throw apiError("The staging response carried no upload URL.", grant);
|
|
2957
|
+
}
|
|
2958
|
+
if (grant.maxBytes !== void 0 && bytes.byteLength > grant.maxBytes) {
|
|
2959
|
+
throw validationError(
|
|
2960
|
+
`${file} is ${bytes.byteLength} bytes; the limit is ${grant.maxBytes}. Push it with git instead: anima get-git-token ${sessionId}`
|
|
2961
|
+
);
|
|
2962
|
+
}
|
|
2963
|
+
progress.update("Uploading...");
|
|
2964
|
+
await postAsset(grant.uploadUrl, grant.uploadFields, bytes, path5.basename(file));
|
|
2965
|
+
logEvent("asset_uploaded", { sessionId, assetUploadId: grant.assetUploadId });
|
|
2966
|
+
if (!opts.path) {
|
|
2967
|
+
progress.succeed("Uploaded \u2014 not committed yet.");
|
|
2968
|
+
if (mode === "pretty") {
|
|
2969
|
+
console.log();
|
|
2970
|
+
printKeyValue("assetUploadId", grant.assetUploadId);
|
|
2971
|
+
console.log();
|
|
2972
|
+
console.log(" Commit it where it belongs with:");
|
|
2973
|
+
console.log(
|
|
2974
|
+
` ${chalk12.cyan(
|
|
2975
|
+
`anima edit ${sessionId} -m "Add asset" --changes '[{"op":"write","path":"public/${path5.basename(file)}","assetUploadId":"${grant.assetUploadId}"}]'`
|
|
2976
|
+
)}`
|
|
2977
|
+
);
|
|
2978
|
+
console.log();
|
|
2979
|
+
} else {
|
|
2980
|
+
outputResult(mode, {
|
|
2981
|
+
success: true,
|
|
2982
|
+
sessionId,
|
|
2983
|
+
assetUploadId: grant.assetUploadId,
|
|
2984
|
+
expiresAt: grant.expiresAt,
|
|
2985
|
+
nextSteps: "Call `anima edit <artifact> --changes` with a write change carrying this assetUploadId and the path the file should live at. The id is single-use and expires."
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
progress.update("Committing...");
|
|
2991
|
+
const baseRevision = opts.baseRevision ?? await currentRevision2(sessionId, opts.apiUrl);
|
|
2992
|
+
const result = await runTool(
|
|
2993
|
+
"artifact-edit",
|
|
2994
|
+
{
|
|
2995
|
+
sessionId,
|
|
2996
|
+
baseRevision,
|
|
2997
|
+
commitMessage: opts.message ?? `Add ${opts.path}`,
|
|
2998
|
+
changes: [{ op: "write", path: opts.path, assetUploadId: grant.assetUploadId }]
|
|
2999
|
+
},
|
|
3000
|
+
{ apiUrl: opts.apiUrl }
|
|
3001
|
+
);
|
|
3002
|
+
progress.succeed("Uploaded and committed!");
|
|
3003
|
+
if (mode === "pretty") {
|
|
3004
|
+
console.log();
|
|
3005
|
+
printSuccess(`${file} \u2192 ${opts.path}`);
|
|
3006
|
+
printKeyValue("Revision", result.revision ?? "unknown");
|
|
3007
|
+
console.log();
|
|
3008
|
+
} else {
|
|
3009
|
+
outputResult(mode, {
|
|
3010
|
+
success: true,
|
|
3011
|
+
sessionId,
|
|
3012
|
+
path: opts.path,
|
|
3013
|
+
assetUploadId: grant.assetUploadId,
|
|
3014
|
+
revision: result.revision
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
});
|
|
3018
|
+
});
|
|
3019
|
+
async function postAsset(uploadUrl, uploadFields, bytes, filename) {
|
|
3020
|
+
const form = new FormData();
|
|
3021
|
+
for (const [key, value] of Object.entries(uploadFields)) {
|
|
3022
|
+
form.append(key, value);
|
|
3023
|
+
}
|
|
3024
|
+
form.append("file", new Blob([new Uint8Array(bytes)]), filename);
|
|
3025
|
+
const response = await fetch(uploadUrl, { method: "POST", body: form });
|
|
3026
|
+
if (!response.ok) {
|
|
3027
|
+
const detail = await response.text().catch(() => "");
|
|
3028
|
+
throw apiError(`Asset upload failed: HTTP ${response.status}${detail ? ` \u2014 ${detail.slice(0, 300)}` : ""}`);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
async function currentRevision2(sessionId, apiUrl) {
|
|
3032
|
+
const result = await runTool(
|
|
3033
|
+
"artifact-explore",
|
|
3034
|
+
{ sessionId, action: "tree", limit: 1 },
|
|
3035
|
+
{ apiUrl }
|
|
3036
|
+
);
|
|
3037
|
+
if (!result.revision) {
|
|
3038
|
+
throw validationError("Could not read the artifact's current revision. Pass --base-revision.");
|
|
3039
|
+
}
|
|
3040
|
+
return result.revision;
|
|
3041
|
+
}
|
|
3042
|
+
function readAsset(file) {
|
|
3043
|
+
try {
|
|
3044
|
+
return fs6.readFileSync(file);
|
|
3045
|
+
} catch (err) {
|
|
3046
|
+
throw validationError(`Could not read ${file}: ${err.message}`);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
// src/commands/delete.ts
|
|
3051
|
+
import { Command as Command20 } from "commander";
|
|
3052
|
+
var deleteCommand = new Command20("delete").summary("Delete an artifact (reversible: hidden, not erased)").description("Soft-delete an artifact only when explicitly requested (unpublish it first; content is retained)").argument("<artifactUrl>", "Artifact URL or session ID to soft-delete").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
|
|
3053
|
+
const mode = detectOutputMode(opts);
|
|
3054
|
+
initLogger(opts.logFile);
|
|
3055
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
3056
|
+
try {
|
|
3057
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
3058
|
+
if (!sessionId) {
|
|
3059
|
+
throw validationError("An artifact URL or session ID is required");
|
|
3060
|
+
}
|
|
3061
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
3062
|
+
progress.start("Deleting artifact...");
|
|
3063
|
+
const result = await withMcp(
|
|
3064
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
3065
|
+
(client) => client.callTool("artifact-delete", { sessionId })
|
|
3066
|
+
);
|
|
3067
|
+
if (!result.success) {
|
|
3068
|
+
throw apiError(result.message || result.reason || result.error || "Artifact deletion failed", result);
|
|
3069
|
+
}
|
|
3070
|
+
progress.succeed("Artifact deleted");
|
|
3071
|
+
if (mode === "pretty") {
|
|
3072
|
+
printSuccess(result.message || `Artifact ${sessionId} is hidden and can be restored.`);
|
|
3073
|
+
} else {
|
|
3074
|
+
outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
|
|
3075
|
+
}
|
|
3076
|
+
} catch (err) {
|
|
3077
|
+
progress.fail("Failed");
|
|
3078
|
+
if (err instanceof CliError) {
|
|
3079
|
+
const detail = err.detail;
|
|
3080
|
+
const code = typeof detail?.code === "string" ? detail.code : err.code;
|
|
3081
|
+
if (mode === "pretty") {
|
|
3082
|
+
printError(code === err.code ? err.message : `${code}: ${err.message}`);
|
|
3083
|
+
} else {
|
|
3084
|
+
outputError(mode, { code, message: err.message });
|
|
3085
|
+
}
|
|
3086
|
+
process.exit(err.exitCode);
|
|
3087
|
+
}
|
|
3088
|
+
throw err;
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
3091
|
+
|
|
3092
|
+
// src/commands/review.ts
|
|
3093
|
+
import { Command as Command21 } from "commander";
|
|
3094
|
+
import chalk13 from "chalk";
|
|
3095
|
+
var listSubcommand = new Command21("list").summary("Read the comments addressed to you").description("Read the review comments addressed to you, newest artifact activity first").argument("[artifactUrl]", "Limit to one artifact; omit for every review addressed to you").option("--limit <n>", "Maximum reviews to return (1-50, default 20)").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
|
|
3096
|
+
const mode = detectOutputMode(opts);
|
|
3097
|
+
initLogger(opts.logFile);
|
|
3098
|
+
await withCommandErrors(mode, async () => {
|
|
3099
|
+
const args = {};
|
|
3100
|
+
if (artifactUrl) args.sessionId = parseArtifactUrl(artifactUrl);
|
|
3101
|
+
if (opts.limit !== void 0) args.limit = parseLimit2(opts.limit);
|
|
3102
|
+
const result = await runTool("review-list", args, { apiUrl: opts.apiUrl });
|
|
3103
|
+
if (mode === "pretty") {
|
|
3104
|
+
printReviews(result);
|
|
3105
|
+
} else {
|
|
3106
|
+
outputResult(mode, result);
|
|
3107
|
+
}
|
|
3108
|
+
});
|
|
3109
|
+
});
|
|
3110
|
+
var resolveSubcommand = new Command21("resolve").summary("Close one comment without a commit").description("Close one comment without a commit \u2014 for a question answered, or a change you decided against").argument("<commentId>", "Comment to close, from `anima review list`").option("--note <text>", "What to tell the reviewer as the closing message").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (commentId, opts) => {
|
|
3111
|
+
const mode = detectOutputMode(opts);
|
|
3112
|
+
initLogger(opts.logFile);
|
|
3113
|
+
await withCommandErrors(mode, async () => {
|
|
3114
|
+
const result = await runTool(
|
|
3115
|
+
"review-resolve",
|
|
3116
|
+
{ commentId, ...opts.note ? { note: opts.note } : {} },
|
|
3117
|
+
{ apiUrl: opts.apiUrl }
|
|
3118
|
+
);
|
|
3119
|
+
if (mode === "pretty") {
|
|
3120
|
+
console.log();
|
|
3121
|
+
if (result.resolved) printSuccess(result.message ?? "Comment resolved.");
|
|
3122
|
+
else printInfo(result.message ?? "Comment was already closed.");
|
|
3123
|
+
console.log();
|
|
3124
|
+
} else {
|
|
3125
|
+
outputResult(mode, result);
|
|
3126
|
+
}
|
|
3127
|
+
});
|
|
3128
|
+
});
|
|
3129
|
+
var replySubcommand = new Command21("reply").summary("Answer one comment, leaving it open").description("Say something back on one comment and LEAVE IT OPEN \u2014 to ask, report, or say you are blocked").argument("<commentId>", "Comment to reply to, from `anima review list`").requiredOption("-m, --message <message>", "What to say to the reviewer").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (commentId, opts) => {
|
|
3130
|
+
const mode = detectOutputMode(opts);
|
|
3131
|
+
initLogger(opts.logFile);
|
|
3132
|
+
await withCommandErrors(mode, async () => {
|
|
3133
|
+
const result = await runTool(
|
|
3134
|
+
"review-reply",
|
|
3135
|
+
{ commentId, message: opts.message },
|
|
3136
|
+
{ apiUrl: opts.apiUrl }
|
|
3137
|
+
);
|
|
3138
|
+
if (mode === "pretty") {
|
|
3139
|
+
console.log();
|
|
3140
|
+
if (result.replied) printSuccess(result.message ?? "Reply posted.");
|
|
3141
|
+
else printInfo(result.message ?? "The comment is closed, so nothing was posted.");
|
|
3142
|
+
console.log();
|
|
3143
|
+
} else {
|
|
3144
|
+
outputResult(mode, result);
|
|
3145
|
+
}
|
|
3146
|
+
});
|
|
3147
|
+
});
|
|
3148
|
+
var reviewCommand = new Command21("review").summary("Read and answer the comments humans addressed to you").description("Read and answer the review comments humans addressed to you (list / resolve / reply)").addCommand(listSubcommand).addCommand(resolveSubcommand).addCommand(replySubcommand);
|
|
3149
|
+
function parseLimit2(value) {
|
|
3150
|
+
const limit = Number(value);
|
|
3151
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
3152
|
+
throw validationError(`--limit must be a whole number between 1 and 50 (got "${value}").`);
|
|
3153
|
+
}
|
|
3154
|
+
return limit;
|
|
3155
|
+
}
|
|
3156
|
+
function printReviews(result) {
|
|
3157
|
+
const reviews = result.reviews ?? [];
|
|
3158
|
+
console.log();
|
|
3159
|
+
if (reviews.length === 0) {
|
|
3160
|
+
console.log(` ${result.message ?? "No open reviews are addressed to you."}`);
|
|
3161
|
+
console.log();
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
for (const review of reviews) {
|
|
3165
|
+
console.log(` ${chalk13.bold(`Review ${review.reviewId}`)} ${chalk13.gray(`by ${review.author.name}`)}`);
|
|
3166
|
+
console.log(` ${chalk13.cyan.underline(review.artifactUrl)}`);
|
|
3167
|
+
if (!review.isLatest) {
|
|
3168
|
+
console.log(
|
|
3169
|
+
chalk13.yellow(" The artifact moved since this was written \u2014 check the comments still apply.")
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
console.log();
|
|
3173
|
+
for (const comment of review.comments) {
|
|
3174
|
+
const marks = [
|
|
3175
|
+
comment.status !== "open" ? chalk13.gray(comment.status) : null,
|
|
3176
|
+
comment.unassigned ? chalk13.yellow("unassigned") : null
|
|
3177
|
+
].filter(Boolean).join(" ");
|
|
3178
|
+
console.log(` ${chalk13.bold(`#${comment.index}`)} ${comment.body}${marks ? ` ${marks}` : ""}`);
|
|
3179
|
+
console.log(chalk13.gray(` ${comment.commentId}${describeAnchor(comment)}`));
|
|
3180
|
+
for (const reply of comment.replies ?? []) {
|
|
3181
|
+
console.log(chalk13.gray(` \u21B3 ${reply.author.name}: ${reply.body}`));
|
|
3182
|
+
}
|
|
3183
|
+
console.log();
|
|
3184
|
+
}
|
|
3185
|
+
printKeyValue("To close these in a commit", "");
|
|
3186
|
+
for (const line of review.resolveTrailer.split("\n")) {
|
|
3187
|
+
console.log(` ${chalk13.gray(line)}`);
|
|
3188
|
+
}
|
|
3189
|
+
console.log();
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
function describeAnchor(comment) {
|
|
3193
|
+
if (!comment.anchor) return " \u2014 about the artifact as a whole";
|
|
3194
|
+
const anchor = comment.anchor;
|
|
3195
|
+
const dom = anchor.dom;
|
|
3196
|
+
const markdown = anchor.markdown;
|
|
3197
|
+
if (dom?.sourceFile) {
|
|
3198
|
+
const instances = typeof dom.instanceCount === "number" && dom.instanceCount > 1 ? dom.instanceCount : 0;
|
|
3199
|
+
return ` \u2014 ${dom.sourceFile}${dom.sourceStart ? `:${dom.sourceStart}` : ""}${instances ? ` (shared markup: ${instances} instances)` : ""}`;
|
|
3200
|
+
}
|
|
3201
|
+
if (markdown?.quote) {
|
|
3202
|
+
return ` \u2014 on "${String(markdown.quote).slice(0, 60)}"`;
|
|
3203
|
+
}
|
|
3204
|
+
return "";
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
// src/commands/skill.ts
|
|
3208
|
+
import { Command as Command22 } from "commander";
|
|
3209
|
+
import fs7 from "fs";
|
|
3210
|
+
import path6 from "path";
|
|
3211
|
+
import chalk14 from "chalk";
|
|
3212
|
+
var GUIDE_PATH = "/guide.md";
|
|
3213
|
+
function createSkillCommand() {
|
|
3214
|
+
return new Command22("skill").summary("Install the AgentGrid guide as a skill your agent can read").description(
|
|
3215
|
+
"Fetch the AgentGrid agent guide and write it as a SKILL.md your runtime can load, or print it to stdout. The guide comes from the API, so it stays current; the command reference is generated from this CLI's own commands."
|
|
3216
|
+
).argument(
|
|
3217
|
+
"[path]",
|
|
3218
|
+
"Where to write the SKILL.md (e.g. .claude/skills/agentgrid/SKILL.md). Omit to print to stdout"
|
|
3219
|
+
).option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (target, opts, command) => {
|
|
3220
|
+
const mode = detectOutputMode(opts);
|
|
3221
|
+
initLogger(opts.logFile);
|
|
3222
|
+
await withCommandErrors(mode, async () => {
|
|
3223
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
3224
|
+
const guide = await fetchGuide(apiUrl);
|
|
3225
|
+
const program2 = command.parent ?? command;
|
|
3226
|
+
const body = [connectSection(), commandReference(program2), guide.trim(), provenance(apiUrl)].join(
|
|
3227
|
+
"\n\n"
|
|
3228
|
+
);
|
|
3229
|
+
const document = `${frontmatter()}
|
|
3230
|
+
${body}
|
|
3231
|
+
`;
|
|
3232
|
+
if (!target) {
|
|
3233
|
+
process.stdout.write(document.endsWith("\n") ? document : `${document}
|
|
3234
|
+
`);
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
writeDocument(target, document);
|
|
3238
|
+
logEvent("skill_installed", { path: target, bytes: document.length });
|
|
3239
|
+
if (mode === "pretty") {
|
|
3240
|
+
console.log();
|
|
3241
|
+
printSuccess(`Skill written to ${chalk14.cyan(target)}`);
|
|
3242
|
+
console.log(` ${chalk14.gray(`${document.length} bytes, from ${apiUrl}${GUIDE_PATH}`)}`);
|
|
3243
|
+
console.log();
|
|
3244
|
+
} else {
|
|
3245
|
+
outputResult(mode, {
|
|
3246
|
+
success: true,
|
|
3247
|
+
path: path6.resolve(target),
|
|
3248
|
+
bytes: document.length,
|
|
3249
|
+
source: `${apiUrl}${GUIDE_PATH}`
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
});
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
var skillCommand = createSkillCommand();
|
|
3256
|
+
async function fetchGuide(apiUrl) {
|
|
3257
|
+
const url = `${apiUrl.replace(/\/$/, "")}${GUIDE_PATH}`;
|
|
3258
|
+
logEvent("skill_fetch", { url });
|
|
3259
|
+
let response;
|
|
3260
|
+
try {
|
|
3261
|
+
response = await fetch(url);
|
|
3262
|
+
} catch (err) {
|
|
3263
|
+
throw apiError(`Could not reach ${url}: ${err.message}`);
|
|
3264
|
+
}
|
|
3265
|
+
if (response.status === 404) {
|
|
3266
|
+
throw apiError(
|
|
3267
|
+
`${url} returned 404. This API does not serve the agent guide yet \u2014 point --api-url at one that does.`
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
if (!response.ok) {
|
|
3271
|
+
throw apiError(`Could not fetch the guide: HTTP ${response.status} from ${url}`);
|
|
3272
|
+
}
|
|
3273
|
+
const body = await response.text();
|
|
3274
|
+
if (!body.trim()) {
|
|
3275
|
+
throw apiError(`${url} returned an empty guide.`);
|
|
3276
|
+
}
|
|
3277
|
+
return body;
|
|
3278
|
+
}
|
|
3279
|
+
function frontmatter() {
|
|
3280
|
+
return [
|
|
3281
|
+
"---",
|
|
3282
|
+
"name: agentgrid",
|
|
3283
|
+
"description: >-",
|
|
3284
|
+
" Host, change, publish and share artifacts on AgentGrid from the command line.",
|
|
3285
|
+
" Use when asked to share an app or a document with a human, put work somewhere",
|
|
3286
|
+
" they can see it, create or edit an AgentGrid artifact, publish one to a live",
|
|
3287
|
+
" URL, or answer review comments left on one.",
|
|
3288
|
+
"---"
|
|
3289
|
+
].join("\n");
|
|
3290
|
+
}
|
|
3291
|
+
function connectSection() {
|
|
3292
|
+
return `# AgentGrid
|
|
3293
|
+
|
|
3294
|
+
An **artifact** is a hosted app or document that is also a real git repository.
|
|
3295
|
+
Every command below runs one AgentGrid tool for you.
|
|
3296
|
+
|
|
3297
|
+
## Connect first
|
|
3298
|
+
|
|
3299
|
+
You act as a scoped agent identity a human approved and can revoke. Three doors \u2014
|
|
3300
|
+
the first is the usual one, because you arrive with something to show and they
|
|
3301
|
+
need no account until they claim it:
|
|
3302
|
+
|
|
3303
|
+
\`\`\`bash
|
|
3304
|
+
# 1. You lead: create with no credential at all, then wait for the claim
|
|
3305
|
+
npx @animaapp/cli@latest create --anonymous -t import --from ./my-project \\
|
|
3306
|
+
--client-name "<your product name>" --json # returns artifactUrl \u2014 send it to your human
|
|
3307
|
+
npx @animaapp/cli@latest login --handoff --json # blocks until they claim; stores your credentials
|
|
3308
|
+
|
|
3309
|
+
# 2. A human is reachable and can approve a short code
|
|
3310
|
+
npx @animaapp/cli@latest login
|
|
3311
|
+
|
|
3312
|
+
# 3. A human already sent you an invite link
|
|
3313
|
+
npx @animaapp/cli@latest login --invite <url-or-code>
|
|
3314
|
+
\`\`\`
|
|
3315
|
+
|
|
3316
|
+
Credentials live in \`~/.config/anima/\` and the CLI renews them for you. Check
|
|
3317
|
+
with \`anima auth --status\`. Every command takes \`--json\` for machine-readable
|
|
3318
|
+
output on stdout, with progress on stderr.`;
|
|
3319
|
+
}
|
|
3320
|
+
function commandReference(program2) {
|
|
3321
|
+
const rows = [];
|
|
3322
|
+
for (const command of program2.commands) {
|
|
3323
|
+
if (command.name() === "help") continue;
|
|
3324
|
+
const subcommands = command.commands.filter((sub) => sub.name() !== "help");
|
|
3325
|
+
if (subcommands.length > 0) {
|
|
3326
|
+
for (const sub of subcommands) {
|
|
3327
|
+
rows.push(row(`${command.name()} ${sub.name()}`, sub));
|
|
3328
|
+
}
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
rows.push(row(command.name(), command));
|
|
3332
|
+
}
|
|
3333
|
+
return [
|
|
3334
|
+
"## Command reference",
|
|
3335
|
+
"",
|
|
3336
|
+
`Generated from \`@animaapp/cli\` ${version}, so this is what your copy accepts.`,
|
|
3337
|
+
"Run `anima <command> --help` for a command's full options.",
|
|
3338
|
+
"",
|
|
3339
|
+
"| Command | What it does |",
|
|
3340
|
+
"| ------- | ------------ |",
|
|
3341
|
+
...rows
|
|
3342
|
+
].join("\n");
|
|
3343
|
+
}
|
|
3344
|
+
function row(name, command) {
|
|
3345
|
+
const usage = command.usage().replace(/\[options\]\s*/, "").trim();
|
|
3346
|
+
const invocation = `anima ${name}${usage ? ` ${usage}` : ""}`;
|
|
3347
|
+
const summary = (command.summary() || command.description() || "").split("\n")[0];
|
|
3348
|
+
return `| \`${invocation}\` | ${escapePipes(summary)} |`;
|
|
3349
|
+
}
|
|
3350
|
+
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
3351
|
+
function provenance(apiUrl) {
|
|
3352
|
+
return `---
|
|
3353
|
+
|
|
3354
|
+
_Guide fetched from ${apiUrl}${GUIDE_PATH}; command reference generated by @animaapp/cli ${version}._`;
|
|
3355
|
+
}
|
|
3356
|
+
function writeDocument(target, document) {
|
|
3357
|
+
const directory = path6.dirname(path6.resolve(target));
|
|
3358
|
+
try {
|
|
3359
|
+
fs7.mkdirSync(directory, { recursive: true });
|
|
3360
|
+
} catch (err) {
|
|
3361
|
+
throw validationError(`Could not create ${directory}: ${err.message}`);
|
|
3362
|
+
}
|
|
3363
|
+
try {
|
|
3364
|
+
fs7.writeFileSync(target, document, "utf-8");
|
|
3365
|
+
} catch (err) {
|
|
3366
|
+
throw validationError(`Could not write ${target}: ${err.message}`);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
|
|
2262
3370
|
// src/index.ts
|
|
2263
|
-
var program = new
|
|
2264
|
-
`AgentGrid CLI \u2014
|
|
2265
|
-
real git repository
|
|
3371
|
+
var program = new Command23().name("anima").description(
|
|
3372
|
+
`AgentGrid CLI \u2014 create, change, publish and share artifacts on AgentGrid.
|
|
3373
|
+
An artifact is a real git repository, and every command below runs one
|
|
3374
|
+
AgentGrid MCP tool for you. The command list is at the bottom; run
|
|
3375
|
+
"anima <command> --help" for a command's own options.
|
|
2266
3376
|
|
|
2267
3377
|
THE MAIN FLOW (build something and put it on AgentGrid):
|
|
2268
|
-
1.
|
|
2269
|
-
2.
|
|
3378
|
+
1. anima login # once; a human approves
|
|
3379
|
+
2. anima create -t import --from ./my-project # your code \u2192 a live app
|
|
2270
3380
|
(or: create -t empty --framework html \u2014 an empty repo you push to)
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
3381
|
+
(can create in several workspaces? add --workspace <id>; see "workspaces")
|
|
3382
|
+
3. Change it. Two doors, same git repository:
|
|
3383
|
+
- no clone: "explore" to read the files, "edit" to commit them. Needs no
|
|
3384
|
+
shell, no git binary and no network of its own \u2014 prefer it.
|
|
3385
|
+
- a real checkout: "get-git-token" (create already returns one), then
|
|
3386
|
+
git clone / commit / push. Worth it for a large refactor, running the
|
|
3387
|
+
project, branches or rewriting history.
|
|
3388
|
+
4. Share the artifactUrl \u2014 app.agentgrid.io/artifacts/<sessionId>. That IS
|
|
3389
|
+
sharing: the artifact is already visible there. Do NOT run "publish"
|
|
3390
|
+
unless a human explicitly asked for a public site; offer it as a
|
|
3391
|
+
follow-up question instead.
|
|
3392
|
+
|
|
3393
|
+
TIMING \u2014 how long the COMMAND blocks, not how long the server takes:
|
|
3394
|
+
create -t p2c|l2c|f2c BLOCKS 3-7 min. The generation itself is
|
|
3395
|
+
asynchronous, but create waits it out for you, so
|
|
3396
|
+
what it prints is a finished app and a failed
|
|
3397
|
+
generation exits non-zero. Keep the default
|
|
3398
|
+
600000ms --timeout. Pass --no-wait to get the
|
|
3399
|
+
sessionId back in seconds and do the waiting
|
|
3400
|
+
yourself with "anima status <sessionId> --wait".
|
|
3401
|
+
publish / unpublish BLOCKS 1-3 min
|
|
3402
|
+
everything else seconds, create -t empty|import included
|
|
2276
3403
|
|
|
2277
3404
|
AUTHENTICATION:
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
update Rename an artifact or set visibility public/private (metadata only)
|
|
2296
|
-
publish Deploy to a public live URL \u2014 ONLY when the human asked for it
|
|
2297
|
-
unpublish Take a published artifact offline (inverse of publish)
|
|
2298
|
-
mcp-config Print an MCP server config from stored credentials (no new login)
|
|
2299
|
-
codegen Convert Figma design to local code files (no artifact)
|
|
2300
|
-
config Store CLI preferences (e.g. a default API URL)
|
|
2301
|
-
auth Inspect or manage credentials (status / figma token)
|
|
2302
|
-
|
|
2303
|
-
EXAMPLES (headless flow):
|
|
2304
|
-
npx @animaapp/cli login --invite https://api.agentgrid.io/invite/<code>.md
|
|
2305
|
-
npx @animaapp/cli create -t import --from ./my-project
|
|
2306
|
-
npx @animaapp/cli create -t empty --framework react --name "My app"
|
|
2307
|
-
npx @animaapp/cli duplicate <artifactUrl> --name "My copy"
|
|
2308
|
-
npx @animaapp/cli get-git-token <sessionId>
|
|
2309
|
-
npx @animaapp/cli update <sessionId> --privacy public
|
|
2310
|
-
npx @animaapp/cli publish <sessionId> # only on explicit request
|
|
2311
|
-
npx @animaapp/cli unpublish <sessionId>
|
|
2312
|
-
|
|
2313
|
-
OUTPUT: JSON to stdout when piped (for AI tools), pretty text in terminals.
|
|
2314
|
-
Progress is streamed to stderr via --verbose when in JSON mode.`
|
|
3405
|
+
anima skill <path> # install this guide where your runtime reads skills
|
|
3406
|
+
anima login # device flow \u2014 a human approves on any device
|
|
3407
|
+
anima login --invite <url> # redeem a pre-approved invite (no approval step)
|
|
3408
|
+
anima create --anonymous \u2026 # no account at all; hand off to a human later
|
|
3409
|
+
Credentials live in ~/.config/anima/; "logout" removes everything.
|
|
3410
|
+
|
|
3411
|
+
EXAMPLES:
|
|
3412
|
+
anima create -t import --from ./my-project
|
|
3413
|
+
anima status <sessionId> --wait
|
|
3414
|
+
anima explore <sessionId> --search "buttonColor"
|
|
3415
|
+
anima edit <sessionId> -m "Blue pill" --replace src/App.tsx --old red --new blue
|
|
3416
|
+
anima review list # what humans asked you to change
|
|
3417
|
+
anima publish <sessionId> # only on explicit request
|
|
3418
|
+
|
|
3419
|
+
OUTPUT: pretty text in a terminal; a single JSON object on stdout when piped
|
|
3420
|
+
or given --json. Progress and prompts go to stderr, so stdout stays clean.
|
|
3421
|
+
Add --verbose to stream progress in JSON mode.`
|
|
2315
3422
|
).version(version, "-v, --version");
|
|
2316
3423
|
program.addCommand(loginCommand);
|
|
2317
3424
|
program.addCommand(logoutCommand);
|
|
3425
|
+
program.addCommand(workspacesCommand);
|
|
2318
3426
|
program.addCommand(listCommand);
|
|
2319
3427
|
program.addCommand(createCommand);
|
|
3428
|
+
program.addCommand(createKnowledgeCommand);
|
|
3429
|
+
program.addCommand(statusCommand);
|
|
3430
|
+
program.addCommand(exploreCommand);
|
|
3431
|
+
program.addCommand(editCommand);
|
|
3432
|
+
program.addCommand(uploadAssetCommand);
|
|
3433
|
+
program.addCommand(getGitTokenCommand);
|
|
3434
|
+
program.addCommand(reviewCommand);
|
|
2320
3435
|
program.addCommand(duplicateCommand);
|
|
2321
3436
|
program.addCommand(publishCommand);
|
|
2322
3437
|
program.addCommand(unpublishCommand);
|
|
2323
3438
|
program.addCommand(updateCommand);
|
|
3439
|
+
program.addCommand(deleteCommand);
|
|
2324
3440
|
program.addCommand(codegenCommand);
|
|
2325
|
-
program.addCommand(
|
|
3441
|
+
program.addCommand(skillCommand);
|
|
2326
3442
|
program.addCommand(mcpConfigCommand);
|
|
2327
3443
|
program.addCommand(configCommand);
|
|
2328
3444
|
program.addCommand(authCommand);
|