@animaapp/cli 0.7.0 → 0.8.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 +278 -61
- package/dist/index.js +1078 -138
- 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 Command21 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/create.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -87,7 +87,7 @@ function readPendingHandoff() {
|
|
|
87
87
|
try {
|
|
88
88
|
if (!fs.existsSync(HANDOFF_FILE)) return null;
|
|
89
89
|
const parsed = JSON.parse(fs.readFileSync(HANDOFF_FILE, "utf-8"));
|
|
90
|
-
const required2 = ["handoffToken", "sessionId", "
|
|
90
|
+
const required2 = ["handoffToken", "sessionId", "artifactUrl", "claimableUntil", "apiUrl"];
|
|
91
91
|
if (required2.some((field) => typeof parsed[field] !== "string")) return null;
|
|
92
92
|
return parsed;
|
|
93
93
|
} catch {
|
|
@@ -608,8 +608,11 @@ function printSessionResult(result) {
|
|
|
608
608
|
console.log();
|
|
609
609
|
printSuccess("Artifact created successfully!");
|
|
610
610
|
printKeyValue("Session ID", result.sessionId);
|
|
611
|
+
if (result.artifactUrl) {
|
|
612
|
+
printKeyValue("Your artifact", chalk.cyan.underline(result.artifactUrl));
|
|
613
|
+
}
|
|
611
614
|
if (result.playgroundUrl) {
|
|
612
|
-
printKeyValue("
|
|
615
|
+
printKeyValue("Edit in Playground", chalk.cyan.underline(result.playgroundUrl));
|
|
613
616
|
}
|
|
614
617
|
if (result.tokenUsage) {
|
|
615
618
|
printKeyValue("Token usage", result.tokenUsage.toLocaleString());
|
|
@@ -710,8 +713,8 @@ function zipFolder(root) {
|
|
|
710
713
|
});
|
|
711
714
|
return zip.toBuffer();
|
|
712
715
|
}
|
|
713
|
-
function isProbablyBinary(
|
|
714
|
-
if (BINARY_EXTENSIONS.has(extname(
|
|
716
|
+
function isProbablyBinary(path7, buffer) {
|
|
717
|
+
if (BINARY_EXTENSIONS.has(extname(path7).toLowerCase())) return true;
|
|
715
718
|
return buffer.includes(0);
|
|
716
719
|
}
|
|
717
720
|
function collectProjectSource(fromPath) {
|
|
@@ -732,30 +735,33 @@ function collectProjectSource(fromPath) {
|
|
|
732
735
|
}
|
|
733
736
|
const files = {};
|
|
734
737
|
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 (${
|
|
738
|
+
for (const path7 of paths) {
|
|
739
|
+
const buffer = readFileSync(join(fromPath, path7));
|
|
740
|
+
if (isProbablyBinary(path7, buffer)) {
|
|
741
|
+
return { kind: "zip", buffer: zipFolder(fromPath), reason: `binary file present (${path7})` };
|
|
739
742
|
}
|
|
740
743
|
totalBytes += buffer.length;
|
|
741
744
|
if (totalBytes > MAX_INLINE_BYTES) {
|
|
742
745
|
return { kind: "zip", buffer: zipFolder(fromPath), reason: "project exceeds the inline size limit" };
|
|
743
746
|
}
|
|
744
|
-
files[
|
|
747
|
+
files[path7.split("\\").join("/")] = buffer.toString("utf8");
|
|
745
748
|
}
|
|
746
749
|
return { kind: "files", files, fileCount: paths.length };
|
|
747
750
|
}
|
|
748
751
|
|
|
749
|
-
// src/utils/
|
|
750
|
-
var
|
|
751
|
-
function
|
|
752
|
+
// src/utils/artifactUrl.ts
|
|
753
|
+
var ARTIFACT_BASE = "https://app.agentgrid.io/artifacts";
|
|
754
|
+
function artifactUrlFor(sessionId) {
|
|
755
|
+
return `${ARTIFACT_BASE}/${sessionId}`;
|
|
756
|
+
}
|
|
757
|
+
function parseArtifactUrl(input) {
|
|
752
758
|
if (!input.includes("/")) {
|
|
753
759
|
return input;
|
|
754
760
|
}
|
|
755
761
|
try {
|
|
756
762
|
const url = new URL(input);
|
|
757
763
|
const parts = url.pathname.split("/").filter(Boolean);
|
|
758
|
-
const markerIndex = parts.findIndex((p) => p === "
|
|
764
|
+
const markerIndex = parts.findIndex((p) => p === "artifacts" || p === "chat");
|
|
759
765
|
if (markerIndex !== -1 && markerIndex + 1 < parts.length) {
|
|
760
766
|
return parts[markerIndex + 1];
|
|
761
767
|
}
|
|
@@ -765,6 +771,57 @@ function parsePlaygroundUrl(input) {
|
|
|
765
771
|
}
|
|
766
772
|
}
|
|
767
773
|
|
|
774
|
+
// src/utils/waitForArtifact.ts
|
|
775
|
+
var STATUS_CALL_TIMEOUT_MS = 12e4;
|
|
776
|
+
async function fetchArtifactStatus(client, sessionId, options = {}) {
|
|
777
|
+
return client.callTool(
|
|
778
|
+
"artifact-status",
|
|
779
|
+
{ sessionId, wait: options.wait ?? false },
|
|
780
|
+
{
|
|
781
|
+
timeoutMs: STATUS_CALL_TIMEOUT_MS,
|
|
782
|
+
onProgress: options.onProgress ? (p) => options.onProgress(p.message ?? `Generating (${Math.round(p.progress * 100)}%)...`) : void 0
|
|
783
|
+
}
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
async function waitForArtifact(client, sessionId, options = { deadline: Date.now() }) {
|
|
787
|
+
for (; ; ) {
|
|
788
|
+
const result = await fetchArtifactStatus(client, sessionId, {
|
|
789
|
+
wait: true,
|
|
790
|
+
onProgress: options.onProgress
|
|
791
|
+
});
|
|
792
|
+
if (!result.success) {
|
|
793
|
+
throw new CliError(
|
|
794
|
+
"API_ERROR",
|
|
795
|
+
result.reason || result.error || "Could not read the artifact's status",
|
|
796
|
+
EXIT_CODES.API_ERROR,
|
|
797
|
+
result
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
if (result.status !== "generating") {
|
|
801
|
+
logEvent("generation_settled", { sessionId, status: result.status });
|
|
802
|
+
return result;
|
|
803
|
+
}
|
|
804
|
+
if (Date.now() >= options.deadline) {
|
|
805
|
+
logEvent("generation_wait_timeout", { sessionId, progress: result.progress });
|
|
806
|
+
throw generationTimeoutError(sessionId, result.progress);
|
|
807
|
+
}
|
|
808
|
+
options.onProgress?.(
|
|
809
|
+
result.progress !== void 0 ? `Generating (${result.progress}%)...` : "Generating..."
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function generationTimeoutError(sessionId, progress) {
|
|
814
|
+
const reached = progress !== void 0 ? ` (reached ${progress}%)` : "";
|
|
815
|
+
return new CliError(
|
|
816
|
+
"TIMEOUT",
|
|
817
|
+
`Still generating${reached} when the wait budget ran out. Generation is NOT cancelled \u2014 it keeps running in the background.
|
|
818
|
+
Pick the wait back up with: anima status ${sessionId} --wait
|
|
819
|
+
Or allow longer next time with --timeout <ms>.`,
|
|
820
|
+
EXIT_CODES.TIMEOUT,
|
|
821
|
+
{ sessionId, progress }
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
768
825
|
// src/api/anonymousCreate.ts
|
|
769
826
|
async function createAnonymousArtifact(request, fetchFn = fetch) {
|
|
770
827
|
const url = `${request.apiUrl.replace(/\/$/, "")}/v1/generationSessions`;
|
|
@@ -792,7 +849,7 @@ async function createAnonymousArtifact(request, fetchFn = fetch) {
|
|
|
792
849
|
throw apiError(nextSteps ? `${message}
|
|
793
850
|
${nextSteps}` : message, payload);
|
|
794
851
|
}
|
|
795
|
-
for (const field of ["sessionId", "handoffToken", "
|
|
852
|
+
for (const field of ["sessionId", "handoffToken", "artifactUrl", "claimableUntil"]) {
|
|
796
853
|
if (typeof payload[field] !== "string") {
|
|
797
854
|
throw apiError(`The API's create response is missing \`${field}\`.`, payload);
|
|
798
855
|
}
|
|
@@ -811,7 +868,7 @@ async function getAnonymousZipUploadUrl(apiUrl, fetchFn = fetch) {
|
|
|
811
868
|
}
|
|
812
869
|
|
|
813
870
|
// src/commands/create.ts
|
|
814
|
-
var createCommand = new Command("create").description(
|
|
871
|
+
var createCommand = new Command("create").summary("Create an artifact: your code, an empty repo, or AI generation").description(
|
|
815
872
|
"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
873
|
).requiredOption(
|
|
817
874
|
"-t, --type <type>",
|
|
@@ -828,7 +885,10 @@ var createCommand = new Command("create").description(
|
|
|
828
885
|
).option(
|
|
829
886
|
"--handoff <token>",
|
|
830
887
|
"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(
|
|
888
|
+
).option("--client-name <name>", "How this CLI is credited as the creator of an anonymous artifact", "AgentGrid CLI").option("--json", "Output as JSON").option(
|
|
889
|
+
"--no-wait",
|
|
890
|
+
"Return as soon as generation starts (p2c/l2c/f2c), instead of waiting for the finished app. Poll with `anima status <sessionId> --wait`"
|
|
891
|
+
).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
892
|
const mode = detectOutputMode(opts);
|
|
833
893
|
initLogger(opts.logFile);
|
|
834
894
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -872,22 +932,44 @@ ${grant.nextSteps}` : message, grant);
|
|
|
872
932
|
progress.start(
|
|
873
933
|
type === "empty" ? "Creating empty artifact..." : type === "import" ? "Importing your project..." : `Creating ${type.toUpperCase()} artifact...`
|
|
874
934
|
);
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
}
|
|
881
|
-
|
|
935
|
+
const timeoutMs = parseInt(opts.timeout, 10);
|
|
936
|
+
const deadline = Date.now() + timeoutMs;
|
|
937
|
+
const { result, settled } = await withMcp(mcpConfig, async (client) => {
|
|
938
|
+
const created = await client.callTool("artifact-create", args, { timeoutMs });
|
|
939
|
+
if (!created.success || !created.sessionId) return { created };
|
|
940
|
+
if (created.status !== "generating" || !opts.wait) return { created };
|
|
941
|
+
progress.update("Generating your app...");
|
|
942
|
+
const status2 = await waitForArtifact(client, created.sessionId, {
|
|
943
|
+
deadline,
|
|
944
|
+
onProgress: (message) => progress.update(message)
|
|
945
|
+
});
|
|
946
|
+
return { created, status: status2 };
|
|
947
|
+
}).then(({ created, status: status2 }) => ({ result: created, settled: status2 }));
|
|
882
948
|
if (!result.success || !result.sessionId) {
|
|
883
949
|
const message = result.reason || result.error || "Generation failed";
|
|
884
950
|
throw apiError(result.nextSteps ? `${message}
|
|
885
951
|
${result.nextSteps}` : message, result);
|
|
886
952
|
}
|
|
887
|
-
|
|
888
|
-
|
|
953
|
+
if (settled?.status === "failed") {
|
|
954
|
+
throw apiError(settled.error || "Generation failed", settled);
|
|
955
|
+
}
|
|
956
|
+
const artifactUrl = settled?.artifactUrl || result.artifactUrl || artifactUrlFor(result.sessionId);
|
|
957
|
+
const playgroundUrl = settled?.playgroundUrl ?? result.playgroundUrl;
|
|
958
|
+
const previewUrl = settled?.previewUrl ?? result.previewUrl;
|
|
959
|
+
const status = settled?.status ?? result.status ?? "ready";
|
|
960
|
+
progress.succeed(settled ? "Your app is ready!" : "Artifact created!");
|
|
889
961
|
if (mode === "pretty") {
|
|
890
|
-
printSessionResult({
|
|
962
|
+
printSessionResult({
|
|
963
|
+
sessionId: result.sessionId,
|
|
964
|
+
artifactUrl,
|
|
965
|
+
playgroundUrl
|
|
966
|
+
});
|
|
967
|
+
if (status === "generating") {
|
|
968
|
+
console.log(
|
|
969
|
+
` Still generating. Wait for it with ${chalk2.cyan(`anima status ${result.sessionId} --wait`)}.`
|
|
970
|
+
);
|
|
971
|
+
console.log();
|
|
972
|
+
}
|
|
891
973
|
if (result.gitRemoteUrl) {
|
|
892
974
|
console.log(` ${chalk2.cyan(`git clone ${result.gitRemoteUrl}`)}`);
|
|
893
975
|
console.log();
|
|
@@ -920,7 +1002,14 @@ ${result.nextSteps}` : message, result);
|
|
|
920
1002
|
outputResult(mode, {
|
|
921
1003
|
success: true,
|
|
922
1004
|
sessionId: result.sessionId,
|
|
923
|
-
|
|
1005
|
+
status,
|
|
1006
|
+
artifactUrl,
|
|
1007
|
+
...playgroundUrl ? { playgroundUrl } : {},
|
|
1008
|
+
...previewUrl ? { previewUrl } : {},
|
|
1009
|
+
...settled?.name ? { name: settled.name } : {},
|
|
1010
|
+
...status === "generating" ? {
|
|
1011
|
+
nextSteps: `Generation is still running. Wait for it with: anima status ${result.sessionId} --wait`
|
|
1012
|
+
} : {},
|
|
924
1013
|
...result.gitRemoteUrl ? { gitRemoteUrl: result.gitRemoteUrl, access: result.access, expiresAt: result.expiresAt } : {},
|
|
925
1014
|
...result.framework ? { framework: result.framework } : {},
|
|
926
1015
|
...result.fileCount !== void 0 ? { fileCount: result.fileCount } : {},
|
|
@@ -979,19 +1068,19 @@ async function runAnonymousCreate(opts, mode, progress) {
|
|
|
979
1068
|
progress.update("Previous claim is no longer open \u2014 starting a new one...");
|
|
980
1069
|
return createAnonymousArtifact(request);
|
|
981
1070
|
}) : await createAnonymousArtifact(request);
|
|
982
|
-
const
|
|
1071
|
+
const artifactUrl = result.artifactUrl;
|
|
983
1072
|
savePendingHandoff({
|
|
984
1073
|
handoffToken: result.handoffToken,
|
|
985
1074
|
sessionId: result.sessionId,
|
|
986
|
-
|
|
1075
|
+
artifactUrl,
|
|
987
1076
|
claimableUntil: result.claimableUntil,
|
|
988
1077
|
apiUrl
|
|
989
1078
|
});
|
|
990
1079
|
progress.succeed("Artifact created!");
|
|
991
1080
|
if (mode === "pretty") {
|
|
992
|
-
printSessionResult({ sessionId: result.sessionId
|
|
1081
|
+
printSessionResult({ sessionId: result.sessionId });
|
|
993
1082
|
console.log(" Send your human this link to claim it:");
|
|
994
|
-
console.log(` ${chalk2.cyan.underline(
|
|
1083
|
+
console.log(` ${chalk2.cyan.underline(artifactUrl)}`);
|
|
995
1084
|
console.log();
|
|
996
1085
|
console.log(` Claimable until ${result.claimableUntil}. Until then the artifact is read-only.`);
|
|
997
1086
|
console.log(` Then run ${chalk2.cyan("anima login --handoff")} to pick up your credentials.`);
|
|
@@ -1001,8 +1090,7 @@ async function runAnonymousCreate(opts, mode, progress) {
|
|
|
1001
1090
|
outputResult(mode, {
|
|
1002
1091
|
success: true,
|
|
1003
1092
|
sessionId: result.sessionId,
|
|
1004
|
-
|
|
1005
|
-
claimUrl,
|
|
1093
|
+
artifactUrl,
|
|
1006
1094
|
claimableUntil: result.claimableUntil,
|
|
1007
1095
|
// Returned, not just stored: it is delivered exactly once and can never
|
|
1008
1096
|
// be re-fetched, so a caller whose config does not outlive the process
|
|
@@ -1012,7 +1100,7 @@ async function runAnonymousCreate(opts, mode, progress) {
|
|
|
1012
1100
|
...result.fileCount !== void 0 ? { fileCount: result.fileCount } : {},
|
|
1013
1101
|
...result.skippedFiles?.length ? { skippedFiles: result.skippedFiles } : {},
|
|
1014
1102
|
...result.notes ? { notes: result.notes } : {},
|
|
1015
|
-
nextSteps: `Send
|
|
1103
|
+
nextSteps: `Send artifactUrl to your human \u2014 that is the artifact's page, and the link that transfers it to them. handoffToken is yours, never theirs: use it to log in (\`anima login --handoff\`, which waits until they claim and then stores your credentials), or to add more artifacts to the same claim (\`anima create --anonymous --handoff <token>\`). Run the login in the background if you have other work.`
|
|
1016
1104
|
});
|
|
1017
1105
|
}
|
|
1018
1106
|
}
|
|
@@ -1124,7 +1212,7 @@ function buildCreateArgs(type, opts) {
|
|
|
1124
1212
|
// src/commands/list.ts
|
|
1125
1213
|
import { Command as Command2 } from "commander";
|
|
1126
1214
|
import chalk3 from "chalk";
|
|
1127
|
-
var listCommand = new Command2("list").aliases(["ls", "artifacts"]).description(
|
|
1215
|
+
var listCommand = new Command2("list").summary("List your team's artifacts, most-recently-updated first").aliases(["ls", "artifacts"]).description(
|
|
1128
1216
|
"List your team's artifacts (most-recently-updated first) so you can resume existing work."
|
|
1129
1217
|
).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) => {
|
|
1130
1218
|
const mode = detectOutputMode(opts);
|
|
@@ -1157,9 +1245,7 @@ var listCommand = new Command2("list").aliases(["ls", "artifacts"]).description(
|
|
|
1157
1245
|
}
|
|
1158
1246
|
console.log(` ${chalk3.gray("updated:")} ${formatUpdatedAt(a.updatedAt)}`);
|
|
1159
1247
|
if (a.sessionId) {
|
|
1160
|
-
console.log(
|
|
1161
|
-
` ${chalk3.cyan.underline(`${PLAYGROUND_BASE}/${a.sessionId}`)}`
|
|
1162
|
-
);
|
|
1248
|
+
console.log(` ${chalk3.cyan.underline(artifactUrlFor(a.sessionId))}`);
|
|
1163
1249
|
}
|
|
1164
1250
|
console.log();
|
|
1165
1251
|
}
|
|
@@ -1174,7 +1260,12 @@ var listCommand = new Command2("list").aliases(["ls", "artifacts"]).description(
|
|
|
1174
1260
|
outputResult(mode, {
|
|
1175
1261
|
success: true,
|
|
1176
1262
|
workspaceId: result.workspaceId,
|
|
1177
|
-
|
|
1263
|
+
// The listing tool returns no URL; deriving it here saves every caller
|
|
1264
|
+
// the same string concatenation, and keeps one definition of the link.
|
|
1265
|
+
artifacts: artifacts.map((a) => ({
|
|
1266
|
+
...a,
|
|
1267
|
+
...a.sessionId ? { artifactUrl: artifactUrlFor(a.sessionId) } : {}
|
|
1268
|
+
})),
|
|
1178
1269
|
truncated: result.truncated ?? false
|
|
1179
1270
|
});
|
|
1180
1271
|
}
|
|
@@ -1198,9 +1289,80 @@ function formatUpdatedAt(iso) {
|
|
|
1198
1289
|
return `${relative2} (${iso})`;
|
|
1199
1290
|
}
|
|
1200
1291
|
|
|
1201
|
-
// src/commands/
|
|
1292
|
+
// src/commands/status.ts
|
|
1202
1293
|
import { Command as Command3 } from "commander";
|
|
1203
|
-
|
|
1294
|
+
import chalk4 from "chalk";
|
|
1295
|
+
var statusCommand = new Command3("status").summary("Check whether an artifact finished generating").description(
|
|
1296
|
+
"Check whether an artifact finished generating. --wait blocks until it is ready or failed."
|
|
1297
|
+
).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) => {
|
|
1298
|
+
const mode = detectOutputMode(opts);
|
|
1299
|
+
initLogger(opts.logFile);
|
|
1300
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
1301
|
+
try {
|
|
1302
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
1303
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
1304
|
+
progress.start(opts.wait ? "Waiting for generation..." : "Checking status...");
|
|
1305
|
+
const result = await withMcp(
|
|
1306
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1307
|
+
(client) => opts.wait ? waitForArtifact(client, sessionId, {
|
|
1308
|
+
deadline: Date.now() + parseInt(opts.timeout, 10),
|
|
1309
|
+
onProgress: (message) => progress.update(message)
|
|
1310
|
+
}) : fetchArtifactStatus(client, sessionId)
|
|
1311
|
+
);
|
|
1312
|
+
if (!result.success) {
|
|
1313
|
+
throw apiError(result.reason || result.error || "Could not read the artifact's status", result);
|
|
1314
|
+
}
|
|
1315
|
+
progress.succeed(`Status: ${result.status}`);
|
|
1316
|
+
if (mode === "pretty") {
|
|
1317
|
+
printArtifactStatus(sessionId, result);
|
|
1318
|
+
} else {
|
|
1319
|
+
outputResult(mode, { ...result, sessionId: result.sessionId ?? sessionId });
|
|
1320
|
+
}
|
|
1321
|
+
if (result.status === "failed") {
|
|
1322
|
+
process.exit(1);
|
|
1323
|
+
}
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
progress.fail("Failed");
|
|
1326
|
+
if (err instanceof CliError) {
|
|
1327
|
+
if (mode === "pretty") {
|
|
1328
|
+
printError(err.message);
|
|
1329
|
+
} else {
|
|
1330
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1331
|
+
}
|
|
1332
|
+
process.exit(err.exitCode);
|
|
1333
|
+
}
|
|
1334
|
+
throw err;
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
function printArtifactStatus(sessionId, result) {
|
|
1338
|
+
console.log();
|
|
1339
|
+
if (result.status === "ready") {
|
|
1340
|
+
printSuccess("Ready!");
|
|
1341
|
+
} else if (result.status === "failed") {
|
|
1342
|
+
printError(`Generation failed: ${result.error ?? "no reason given"}`);
|
|
1343
|
+
}
|
|
1344
|
+
printKeyValue("Session ID", result.sessionId ?? sessionId);
|
|
1345
|
+
printKeyValue("Status", result.status ?? "unknown");
|
|
1346
|
+
if (result.name) printKeyValue("Name", result.name);
|
|
1347
|
+
if (result.status === "generating" && result.progress !== void 0) {
|
|
1348
|
+
printKeyValue("Progress", `${result.progress}%`);
|
|
1349
|
+
}
|
|
1350
|
+
if (result.artifactUrl) {
|
|
1351
|
+
printKeyValue("Your artifact", chalk4.cyan.underline(result.artifactUrl));
|
|
1352
|
+
}
|
|
1353
|
+
if (result.playgroundUrl) {
|
|
1354
|
+
printKeyValue("Edit in Playground", chalk4.cyan.underline(result.playgroundUrl));
|
|
1355
|
+
}
|
|
1356
|
+
if (result.nextStep) {
|
|
1357
|
+
console.log();
|
|
1358
|
+
console.log(` Still generating \u2014 run this again with --wait to keep waiting.`);
|
|
1359
|
+
}
|
|
1360
|
+
console.log();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/commands/publish.ts
|
|
1364
|
+
import { Command as Command4 } from "commander";
|
|
1365
|
+
var publishCommand = new Command4("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) => {
|
|
1204
1366
|
const mode = detectOutputMode(opts);
|
|
1205
1367
|
initLogger(opts.logFile);
|
|
1206
1368
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -1246,7 +1408,7 @@ var publishCommand = new Command3("publish").description("Deploy an artifact's a
|
|
|
1246
1408
|
});
|
|
1247
1409
|
|
|
1248
1410
|
// src/commands/codegen.ts
|
|
1249
|
-
import { Command as
|
|
1411
|
+
import { Command as Command5 } from "commander";
|
|
1250
1412
|
|
|
1251
1413
|
// src/utils/files.ts
|
|
1252
1414
|
import fs4 from "fs";
|
|
@@ -1274,7 +1436,7 @@ function writeFilesToDisk(files, outputDir) {
|
|
|
1274
1436
|
}
|
|
1275
1437
|
|
|
1276
1438
|
// src/commands/codegen.ts
|
|
1277
|
-
var codegenCommand = new
|
|
1439
|
+
var codegenCommand = new Command5("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) => {
|
|
1278
1440
|
const mode = detectOutputMode(opts);
|
|
1279
1441
|
initLogger(opts.logFile);
|
|
1280
1442
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -1346,8 +1508,8 @@ var codegenCommand = new Command4("codegen").description("Convert Figma design t
|
|
|
1346
1508
|
});
|
|
1347
1509
|
|
|
1348
1510
|
// src/commands/getGitToken.ts
|
|
1349
|
-
import { Command as
|
|
1350
|
-
import
|
|
1511
|
+
import { Command as Command6 } from "commander";
|
|
1512
|
+
import chalk5 from "chalk";
|
|
1351
1513
|
|
|
1352
1514
|
// src/utils/gitToken.ts
|
|
1353
1515
|
function preview(payload) {
|
|
@@ -1376,17 +1538,29 @@ function parseGitTokenResponse(payload) {
|
|
|
1376
1538
|
return {
|
|
1377
1539
|
gitRemoteUrl: body.gitRemoteUrl,
|
|
1378
1540
|
access: body.access === "rw" ? "rw" : "ro",
|
|
1379
|
-
expiresAt: body.expiresAt
|
|
1541
|
+
expiresAt: body.expiresAt,
|
|
1542
|
+
...commitAuthorOf(body.commitAuthor) ? { commitAuthor: commitAuthorOf(body.commitAuthor) } : {}
|
|
1380
1543
|
};
|
|
1381
1544
|
}
|
|
1545
|
+
function commitAuthorOf(value) {
|
|
1546
|
+
if (!value || typeof value.name !== "string" || typeof value.email !== "string") return void 0;
|
|
1547
|
+
if (!value.name || !value.email) return void 0;
|
|
1548
|
+
return { name: value.name, email: value.email };
|
|
1549
|
+
}
|
|
1550
|
+
function shellQuote(value) {
|
|
1551
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
1552
|
+
}
|
|
1553
|
+
function gitConfigCommands(commitAuthor) {
|
|
1554
|
+
return `git config user.name ${shellQuote(commitAuthor.name)} && git config user.email ${shellQuote(commitAuthor.email)}`;
|
|
1555
|
+
}
|
|
1382
1556
|
|
|
1383
1557
|
// src/commands/getGitToken.ts
|
|
1384
|
-
var getGitTokenCommand = new
|
|
1558
|
+
var getGitTokenCommand = new Command6("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) => {
|
|
1385
1559
|
const mode = detectOutputMode(opts);
|
|
1386
1560
|
initLogger(opts.logFile);
|
|
1387
1561
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
1388
1562
|
try {
|
|
1389
|
-
const sessionId =
|
|
1563
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
1390
1564
|
const auth = await resolveAuth(opts.apiUrl);
|
|
1391
1565
|
progress.start("Requesting git access...");
|
|
1392
1566
|
const response = await withMcp(
|
|
@@ -1398,9 +1572,14 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1398
1572
|
progress.succeed(`Git access granted (${grant.access === "rw" ? "read-write" : "read-only"})`);
|
|
1399
1573
|
if (mode === "pretty") {
|
|
1400
1574
|
console.log();
|
|
1401
|
-
console.log(` ${
|
|
1575
|
+
console.log(` ${chalk5.cyan(`git clone ${grant.gitRemoteUrl}`)}`);
|
|
1402
1576
|
console.log();
|
|
1403
1577
|
if (grant.access === "rw") {
|
|
1578
|
+
if (grant.commitAuthor) {
|
|
1579
|
+
console.log(" In the clone, set who your commits are from:");
|
|
1580
|
+
console.log(` ${chalk5.cyan(gitConfigCommands(grant.commitAuthor))}`);
|
|
1581
|
+
console.log();
|
|
1582
|
+
}
|
|
1404
1583
|
console.log(" Commit and push to update the live artifact.");
|
|
1405
1584
|
} else {
|
|
1406
1585
|
console.log(" Read-only: cloning works, pushing will be rejected.");
|
|
@@ -1408,7 +1587,7 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1408
1587
|
if (grant.expiresAt) {
|
|
1409
1588
|
console.log(
|
|
1410
1589
|
` The URL embeds a token that expires at ${grant.expiresAt} and cannot be renewed;
|
|
1411
|
-
re-run this command and ${
|
|
1590
|
+
re-run this command and ${chalk5.cyan("git remote set-url origin <new url>")} to keep pushing.`
|
|
1412
1591
|
);
|
|
1413
1592
|
}
|
|
1414
1593
|
console.log(" Treat the URL as a secret \u2014 it grants access to this artifact.");
|
|
@@ -1419,7 +1598,11 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1419
1598
|
sessionId,
|
|
1420
1599
|
gitRemoteUrl: grant.gitRemoteUrl,
|
|
1421
1600
|
access: grant.access,
|
|
1422
|
-
expiresAt: grant.expiresAt
|
|
1601
|
+
expiresAt: grant.expiresAt,
|
|
1602
|
+
...grant.commitAuthor ? {
|
|
1603
|
+
commitAuthor: grant.commitAuthor,
|
|
1604
|
+
gitConfigCommand: gitConfigCommands(grant.commitAuthor)
|
|
1605
|
+
} : {}
|
|
1423
1606
|
});
|
|
1424
1607
|
}
|
|
1425
1608
|
} catch (err) {
|
|
@@ -1437,11 +1620,11 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1437
1620
|
});
|
|
1438
1621
|
|
|
1439
1622
|
// src/commands/mcpConfig.ts
|
|
1440
|
-
import { Command as
|
|
1623
|
+
import { Command as Command8 } from "commander";
|
|
1441
1624
|
|
|
1442
1625
|
// src/commands/login.ts
|
|
1443
|
-
import { Command as
|
|
1444
|
-
import
|
|
1626
|
+
import { Command as Command7 } from "commander";
|
|
1627
|
+
import chalk6 from "chalk";
|
|
1445
1628
|
import open from "open";
|
|
1446
1629
|
|
|
1447
1630
|
// src/auth/device.ts
|
|
@@ -1631,7 +1814,7 @@ function resolveDeadline(claimableUntil, now) {
|
|
|
1631
1814
|
}
|
|
1632
1815
|
|
|
1633
1816
|
// src/commands/login.ts
|
|
1634
|
-
var loginCommand = new
|
|
1817
|
+
var loginCommand = new Command7("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(
|
|
1635
1818
|
"--handoff [token]",
|
|
1636
1819
|
"Log in with the handoff token from an anonymous create. Waits until a human claims the artifact. Defaults to your last one"
|
|
1637
1820
|
).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(
|
|
@@ -1701,9 +1884,9 @@ var loginCommand = new Command6("login").description("Log in via the device flow
|
|
|
1701
1884
|
console.log();
|
|
1702
1885
|
printInfo("To connect, open the verification page and enter this code:");
|
|
1703
1886
|
console.log();
|
|
1704
|
-
console.log(` ${
|
|
1887
|
+
console.log(` ${chalk6.cyan.underline(info.verificationUri)}`);
|
|
1705
1888
|
console.log();
|
|
1706
|
-
console.log(` ${
|
|
1889
|
+
console.log(` ${chalk6.bold.white(info.userCode)}`);
|
|
1707
1890
|
console.log();
|
|
1708
1891
|
printInfo(`Waiting for approval\u2026 (code expires in ${Math.round(info.expiresInSeconds / 60)} minutes)`);
|
|
1709
1892
|
} else {
|
|
@@ -1763,7 +1946,7 @@ var loginCommand = new Command6("login").description("Log in via the device flow
|
|
|
1763
1946
|
async function runHandoffLogin(opts, mode) {
|
|
1764
1947
|
const explicitToken = typeof opts.handoff === "string" ? opts.handoff : void 0;
|
|
1765
1948
|
const stored = readPendingHandoff();
|
|
1766
|
-
const pending = stored && (!explicitToken || stored.handoffToken === explicitToken) ? stored : explicitToken ? { handoffToken: explicitToken, sessionId: "",
|
|
1949
|
+
const pending = stored && (!explicitToken || stored.handoffToken === explicitToken) ? stored : explicitToken ? { handoffToken: explicitToken, sessionId: "", artifactUrl: "", claimableUntil: "", apiUrl: resolveApiUrl() } : null;
|
|
1767
1950
|
if (!pending) {
|
|
1768
1951
|
const message = "No handoff token. Pass one (`anima login --handoff <token>`), or run `anima create --anonymous -t import --from <path>` to get one. (Already logged in another way? `anima auth --status`.)";
|
|
1769
1952
|
if (mode === "pretty") {
|
|
@@ -1778,9 +1961,9 @@ async function runHandoffLogin(opts, mode) {
|
|
|
1778
1961
|
if (mode === "pretty") {
|
|
1779
1962
|
console.log();
|
|
1780
1963
|
printInfo("Waiting for a human to claim the artifact:");
|
|
1781
|
-
if (pending.
|
|
1964
|
+
if (pending.artifactUrl) {
|
|
1782
1965
|
console.log();
|
|
1783
|
-
console.log(` ${
|
|
1966
|
+
console.log(` ${chalk6.cyan.underline(pending.artifactUrl)}`);
|
|
1784
1967
|
}
|
|
1785
1968
|
console.log();
|
|
1786
1969
|
printInfo(
|
|
@@ -1790,7 +1973,7 @@ async function runHandoffLogin(opts, mode) {
|
|
|
1790
1973
|
process.stderr.write(
|
|
1791
1974
|
JSON.stringify({
|
|
1792
1975
|
event: "claim_required",
|
|
1793
|
-
...pending.
|
|
1976
|
+
...pending.artifactUrl ? { artifactUrl: pending.artifactUrl } : {},
|
|
1794
1977
|
...pending.sessionId ? { sessionId: pending.sessionId } : {},
|
|
1795
1978
|
...pending.claimableUntil ? { claimableUntil: pending.claimableUntil } : {}
|
|
1796
1979
|
}) + "\n"
|
|
@@ -1855,7 +2038,7 @@ function buildMcpConfig(apiUrl) {
|
|
|
1855
2038
|
}
|
|
1856
2039
|
|
|
1857
2040
|
// src/commands/mcpConfig.ts
|
|
1858
|
-
var mcpConfigCommand = new
|
|
2041
|
+
var mcpConfigCommand = new Command8("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) => {
|
|
1859
2042
|
const mode = detectOutputMode(opts);
|
|
1860
2043
|
try {
|
|
1861
2044
|
const mcpConfig = buildMcpConfig(resolveApiUrl(opts.apiUrl));
|
|
@@ -1882,16 +2065,16 @@ var mcpConfigCommand = new Command7("mcp-config").description("Print an MCP serv
|
|
|
1882
2065
|
});
|
|
1883
2066
|
|
|
1884
2067
|
// src/commands/auth.ts
|
|
1885
|
-
import { Command as
|
|
1886
|
-
import
|
|
2068
|
+
import { Command as Command9 } from "commander";
|
|
2069
|
+
import chalk7 from "chalk";
|
|
1887
2070
|
var DEFAULT_API_URL2 = "https://api.agentgrid.io";
|
|
1888
2071
|
function describePendingHandoff(pending, state) {
|
|
1889
2072
|
return {
|
|
1890
2073
|
state,
|
|
1891
2074
|
sessionId: pending.sessionId,
|
|
1892
|
-
|
|
2075
|
+
artifactUrl: pending.artifactUrl,
|
|
1893
2076
|
claimableUntil: pending.claimableUntil,
|
|
1894
|
-
nextSteps: state === "expired" ? "The claim window closed. Create another artifact, or ask your human for an agent invite: anima login --invite <url-or-code>" : "Send
|
|
2077
|
+
nextSteps: state === "expired" ? "The claim window closed. Create another artifact, or ask your human for an agent invite: anima login --invite <url-or-code>" : "Send artifactUrl to your human, then run `anima login --handoff` to wait for the credentials their claim grants."
|
|
1895
2078
|
};
|
|
1896
2079
|
}
|
|
1897
2080
|
function printPendingHandoff(pending, state) {
|
|
@@ -1901,10 +2084,10 @@ function printPendingHandoff(pending, state) {
|
|
|
1901
2084
|
return;
|
|
1902
2085
|
}
|
|
1903
2086
|
printInfo(`Anonymous artifact ${pending.sessionId} is awaiting a claim (until ${pending.claimableUntil}):`);
|
|
1904
|
-
console.log(` ${
|
|
1905
|
-
console.log(` Run ${
|
|
2087
|
+
console.log(` ${chalk7.cyan.underline(pending.artifactUrl)}`);
|
|
2088
|
+
console.log(` Run ${chalk7.cyan("anima login --handoff")} to wait for it.`);
|
|
1906
2089
|
}
|
|
1907
|
-
var authCommand = new
|
|
2090
|
+
var authCommand = new Command9("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) => {
|
|
1908
2091
|
const mode = detectOutputMode(opts);
|
|
1909
2092
|
try {
|
|
1910
2093
|
if (opts.token || opts.figmaToken) {
|
|
@@ -2004,13 +2187,13 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
|
|
|
2004
2187
|
if (mode === "pretty") {
|
|
2005
2188
|
console.log();
|
|
2006
2189
|
printInfo("Auth commands:");
|
|
2007
|
-
console.log(` ${
|
|
2008
|
-
console.log(` ${
|
|
2009
|
-
console.log(` ${
|
|
2010
|
-
console.log(` ${
|
|
2011
|
-
console.log(` ${
|
|
2190
|
+
console.log(` ${chalk7.cyan("anima login")} Log in (device flow \u2014 approve on any browser)`);
|
|
2191
|
+
console.log(` ${chalk7.cyan("anima login --handoff")} Wait for a human to claim your anonymous artifact`);
|
|
2192
|
+
console.log(` ${chalk7.cyan("anima logout")} Clear all credentials`);
|
|
2193
|
+
console.log(` ${chalk7.cyan("anima auth --status")} Check auth status`);
|
|
2194
|
+
console.log(` ${chalk7.cyan("anima auth --figma-token <T>")} Save Figma token (needed for codegen/f2c)`);
|
|
2012
2195
|
console.log();
|
|
2013
|
-
console.log(` Figma token: ${
|
|
2196
|
+
console.log(` Figma token: ${chalk7.cyan.underline("https://www.figma.com")} > Settings > Personal access tokens`);
|
|
2014
2197
|
console.log();
|
|
2015
2198
|
} else {
|
|
2016
2199
|
outputResult(mode, {
|
|
@@ -2032,8 +2215,8 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
|
|
|
2032
2215
|
});
|
|
2033
2216
|
|
|
2034
2217
|
// src/commands/logout.ts
|
|
2035
|
-
import { Command as
|
|
2036
|
-
var logoutCommand = new
|
|
2218
|
+
import { Command as Command10 } from "commander";
|
|
2219
|
+
var logoutCommand = new Command10("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) => {
|
|
2037
2220
|
const mode = detectOutputMode(opts);
|
|
2038
2221
|
const revoked = await revokeStoredRefreshToken(resolveApiUrl(opts.apiUrl));
|
|
2039
2222
|
const hadCredentials = clearStorage();
|
|
@@ -2051,8 +2234,8 @@ var logoutCommand = new Command9("logout").description("Clear all stored credent
|
|
|
2051
2234
|
});
|
|
2052
2235
|
|
|
2053
2236
|
// src/commands/unpublish.ts
|
|
2054
|
-
import { Command as
|
|
2055
|
-
var unpublishCommand = new
|
|
2237
|
+
import { Command as Command11 } from "commander";
|
|
2238
|
+
var unpublishCommand = new Command11("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) => {
|
|
2056
2239
|
const mode = detectOutputMode(opts);
|
|
2057
2240
|
initLogger(opts.logFile);
|
|
2058
2241
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -2087,8 +2270,8 @@ var unpublishCommand = new Command10("unpublish").description("Take a published
|
|
|
2087
2270
|
});
|
|
2088
2271
|
|
|
2089
2272
|
// src/commands/update.ts
|
|
2090
|
-
import { Command as
|
|
2091
|
-
var updateCommand = new
|
|
2273
|
+
import { Command as Command12 } from "commander";
|
|
2274
|
+
var updateCommand = new Command12("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) => {
|
|
2092
2275
|
const mode = detectOutputMode(opts);
|
|
2093
2276
|
initLogger(opts.logFile);
|
|
2094
2277
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -2133,8 +2316,8 @@ var updateCommand = new Command11("update").description("Update an artifact's me
|
|
|
2133
2316
|
});
|
|
2134
2317
|
|
|
2135
2318
|
// src/commands/config.ts
|
|
2136
|
-
import { Command as
|
|
2137
|
-
var configCommand = new
|
|
2319
|
+
import { Command as Command13 } from "commander";
|
|
2320
|
+
var configCommand = new Command13("config").summary("Store CLI preferences (e.g. a default API URL)").description(
|
|
2138
2321
|
`Store CLI preferences (keys: ${configKeys().join(", ")})`
|
|
2139
2322
|
);
|
|
2140
2323
|
configCommand.command("set <key> <value>").description("Set a config value").option("--json", "Output as JSON").action((key, value, opts) => {
|
|
@@ -2200,10 +2383,10 @@ function assertUrl(value) {
|
|
|
2200
2383
|
}
|
|
2201
2384
|
|
|
2202
2385
|
// src/commands/duplicate.ts
|
|
2203
|
-
import { Command as
|
|
2204
|
-
import
|
|
2386
|
+
import { Command as Command14 } from "commander";
|
|
2387
|
+
import chalk8 from "chalk";
|
|
2205
2388
|
function buildDuplicateArgs(artifactUrl, name) {
|
|
2206
|
-
const sessionId =
|
|
2389
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
2207
2390
|
if (!sessionId) {
|
|
2208
2391
|
throw validationError("A source artifact URL or session ID is required");
|
|
2209
2392
|
}
|
|
@@ -2213,7 +2396,7 @@ function buildDuplicateArgs(artifactUrl, name) {
|
|
|
2213
2396
|
return { sessionId, ...name !== void 0 ? { name } : {} };
|
|
2214
2397
|
}
|
|
2215
2398
|
function createDuplicateCommand() {
|
|
2216
|
-
return new
|
|
2399
|
+
return new Command14("duplicate").summary("Copy an artifact into a new, independent one").description(
|
|
2217
2400
|
"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."
|
|
2218
2401
|
).argument("<artifactUrl>", "Source artifact URL or session ID").option("--name <name>", "Display name for the duplicate (defaults to '<source name> (Copy)')").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) => {
|
|
2219
2402
|
const mode = detectOutputMode(opts);
|
|
@@ -2237,8 +2420,13 @@ function createDuplicateCommand() {
|
|
|
2237
2420
|
printKeyValue("Name", result.name);
|
|
2238
2421
|
printKeyValue("Session ID", result.sessionId);
|
|
2239
2422
|
printKeyValue("Source session", result.sourceSessionId);
|
|
2240
|
-
printKeyValue("Artifact",
|
|
2241
|
-
|
|
2423
|
+
printKeyValue("Artifact", chalk8.cyan.underline(result.artifactUrl));
|
|
2424
|
+
if (result.playgroundUrl) {
|
|
2425
|
+
printKeyValue("Edit in Playground", chalk8.cyan.underline(result.playgroundUrl));
|
|
2426
|
+
}
|
|
2427
|
+
if (result.previewUrl) {
|
|
2428
|
+
printKeyValue("Preview", chalk8.cyan.underline(result.previewUrl));
|
|
2429
|
+
}
|
|
2242
2430
|
printKeyValue("Next steps", result.nextSteps);
|
|
2243
2431
|
console.log();
|
|
2244
2432
|
} else {
|
|
@@ -2260,70 +2448,822 @@ function createDuplicateCommand() {
|
|
|
2260
2448
|
}
|
|
2261
2449
|
var duplicateCommand = createDuplicateCommand();
|
|
2262
2450
|
|
|
2451
|
+
// src/commands/explore.ts
|
|
2452
|
+
import { Command as Command15 } from "commander";
|
|
2453
|
+
import chalk9 from "chalk";
|
|
2454
|
+
|
|
2455
|
+
// src/api/tool.ts
|
|
2456
|
+
async function runTool(toolName, args, options = {}) {
|
|
2457
|
+
const auth = await resolveAuth(options.apiUrl);
|
|
2458
|
+
const result = await withMcp(
|
|
2459
|
+
{
|
|
2460
|
+
apiBaseUrl: resolveApiUrl(options.apiUrl),
|
|
2461
|
+
token: auth.token,
|
|
2462
|
+
teamId: auth.teamId,
|
|
2463
|
+
...options.figmaToken ? { figmaToken: options.figmaToken } : {}
|
|
2464
|
+
},
|
|
2465
|
+
(client) => client.callTool(toolName, args, options.call)
|
|
2466
|
+
);
|
|
2467
|
+
if (result.success === false) {
|
|
2468
|
+
throw apiError(describeFailure(result), result);
|
|
2469
|
+
}
|
|
2470
|
+
return result;
|
|
2471
|
+
}
|
|
2472
|
+
function describeFailure(result) {
|
|
2473
|
+
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";
|
|
2474
|
+
const nextSteps = Array.isArray(result.nextSteps) ? result.nextSteps.join("\n") : result.nextSteps;
|
|
2475
|
+
return nextSteps ? `${base}
|
|
2476
|
+
${nextSteps}` : base;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// src/commands/runner.ts
|
|
2480
|
+
async function withCommandErrors(mode, fn) {
|
|
2481
|
+
try {
|
|
2482
|
+
await fn();
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
if (err instanceof CliError) {
|
|
2485
|
+
if (mode === "pretty") {
|
|
2486
|
+
printError(err.message);
|
|
2487
|
+
} else {
|
|
2488
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
2489
|
+
}
|
|
2490
|
+
process.exit(err.exitCode);
|
|
2491
|
+
}
|
|
2492
|
+
throw err;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// src/commands/explore.ts
|
|
2497
|
+
var exploreCommand = new Command15("explore").summary("Read an artifact's files: list, search, read, history").description(
|
|
2498
|
+
"Inspect an artifact's files without git: list them (--tree), search them (--search), read them (--read), or show commits (--history)."
|
|
2499
|
+
).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) => {
|
|
2500
|
+
const mode = detectOutputMode(opts);
|
|
2501
|
+
initLogger(opts.logFile);
|
|
2502
|
+
await withCommandErrors(mode, async () => {
|
|
2503
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
2504
|
+
const args = buildExploreArgs(sessionId, opts);
|
|
2505
|
+
const result = await runTool("artifact-explore", args, { apiUrl: opts.apiUrl });
|
|
2506
|
+
if (mode === "pretty") {
|
|
2507
|
+
printExplore(args.action, result);
|
|
2508
|
+
} else {
|
|
2509
|
+
outputResult(mode, result);
|
|
2510
|
+
}
|
|
2511
|
+
});
|
|
2512
|
+
});
|
|
2513
|
+
function buildExploreArgs(sessionId, opts) {
|
|
2514
|
+
const chosen = [
|
|
2515
|
+
opts.tree ? "tree" : null,
|
|
2516
|
+
opts.search ? "search" : null,
|
|
2517
|
+
opts.read ? "read" : null,
|
|
2518
|
+
opts.history ? "history" : null
|
|
2519
|
+
].filter(Boolean);
|
|
2520
|
+
if (chosen.length !== 1) {
|
|
2521
|
+
throw validationError(
|
|
2522
|
+
"Pass exactly one of --tree, --search <query>, --read <paths...> or --history. Not sure which file to change? Start with --search."
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
const action = chosen[0];
|
|
2526
|
+
const args = { sessionId, action };
|
|
2527
|
+
if (opts.revision) args.revision = opts.revision;
|
|
2528
|
+
if (opts.limit !== void 0) args.limit = parseLimit(opts.limit);
|
|
2529
|
+
switch (action) {
|
|
2530
|
+
case "tree":
|
|
2531
|
+
if (opts.path) args.path = opts.path;
|
|
2532
|
+
if (opts.includeExcluded) args.includeExcluded = true;
|
|
2533
|
+
break;
|
|
2534
|
+
case "search":
|
|
2535
|
+
args.query = opts.search;
|
|
2536
|
+
if (opts.path) args.path = opts.path;
|
|
2537
|
+
if (opts.regex) args.regex = true;
|
|
2538
|
+
if (opts.caseSensitive) args.caseSensitive = true;
|
|
2539
|
+
if (opts.includeExcluded) args.includeExcluded = true;
|
|
2540
|
+
break;
|
|
2541
|
+
case "read": {
|
|
2542
|
+
const paths = opts.read;
|
|
2543
|
+
args.paths = paths;
|
|
2544
|
+
if (opts.range) {
|
|
2545
|
+
if (paths.length !== 1) {
|
|
2546
|
+
throw validationError("--range applies to a single file; pass one path to --read.");
|
|
2547
|
+
}
|
|
2548
|
+
args.range = opts.range;
|
|
2549
|
+
}
|
|
2550
|
+
break;
|
|
2551
|
+
}
|
|
2552
|
+
case "history":
|
|
2553
|
+
if (opts.cursor) args.cursor = opts.cursor;
|
|
2554
|
+
break;
|
|
2555
|
+
}
|
|
2556
|
+
return args;
|
|
2557
|
+
}
|
|
2558
|
+
function parseLimit(value) {
|
|
2559
|
+
const limit = Number(value);
|
|
2560
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
2561
|
+
throw validationError(`--limit must be a positive whole number (got "${value}").`);
|
|
2562
|
+
}
|
|
2563
|
+
return limit;
|
|
2564
|
+
}
|
|
2565
|
+
function printExplore(action, result) {
|
|
2566
|
+
console.log();
|
|
2567
|
+
if (action === "tree") {
|
|
2568
|
+
for (const file of result.files ?? []) {
|
|
2569
|
+
const kind = result.unwritable?.[file];
|
|
2570
|
+
console.log(` ${file}${kind ? chalk9.gray(` (${kind} \u2014 git only)`) : ""}`);
|
|
2571
|
+
}
|
|
2572
|
+
if (result.matchedFiles !== void 0) {
|
|
2573
|
+
console.log();
|
|
2574
|
+
console.log(chalk9.gray(` ${(result.files ?? []).length} of ${result.matchedFiles} shown`));
|
|
2575
|
+
}
|
|
2576
|
+
} else if (action === "search") {
|
|
2577
|
+
for (const match of result.matches ?? []) {
|
|
2578
|
+
console.log(` ${chalk9.cyan(match.path)}:${match.line} ${match.lineText.trim()}`);
|
|
2579
|
+
}
|
|
2580
|
+
if (!result.matches?.length) {
|
|
2581
|
+
console.log(" No matches.");
|
|
2582
|
+
const skipped = result.notSearched;
|
|
2583
|
+
if (skipped?.excluded) {
|
|
2584
|
+
console.log(
|
|
2585
|
+
chalk9.yellow(
|
|
2586
|
+
` ${skipped.excluded} files were skipped (node_modules, dist, build) \u2014 retry with --include-excluded.`
|
|
2587
|
+
)
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
if (skipped?.assets) {
|
|
2591
|
+
console.log(chalk9.gray(` ${skipped.assets} binary files cannot match a text query.`));
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
} else if (action === "read") {
|
|
2595
|
+
for (const file of result.files ?? []) {
|
|
2596
|
+
console.log(chalk9.bold(` ${file.path}`));
|
|
2597
|
+
if (file.asset) {
|
|
2598
|
+
console.log(chalk9.gray(` binary \u2014 ${file.size ?? "?"} bytes, ${file.mime ?? "unknown type"}`));
|
|
2599
|
+
} else if (file.deferred) {
|
|
2600
|
+
console.log(chalk9.yellow(" not returned: the response ran out of budget \u2014 read it on its own"));
|
|
2601
|
+
} else {
|
|
2602
|
+
console.log(file.content ?? "");
|
|
2603
|
+
if (file.truncated) {
|
|
2604
|
+
console.log(
|
|
2605
|
+
chalk9.yellow(
|
|
2606
|
+
` lines ${file.startLine}-${file.endLine} of ${file.totalLines} \u2014 read the rest with --range`
|
|
2607
|
+
)
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
console.log();
|
|
2612
|
+
}
|
|
2613
|
+
for (const missing of result.missingPaths ?? []) {
|
|
2614
|
+
const near = result.suggestions?.[missing];
|
|
2615
|
+
console.log(chalk9.yellow(` Not found: ${missing}${near?.length ? ` \u2014 did you mean ${near.join(", ")}?` : ""}`));
|
|
2616
|
+
}
|
|
2617
|
+
} else {
|
|
2618
|
+
const commits = result.commit ? [result.commit] : result.commits ?? [];
|
|
2619
|
+
for (const commit of commits) {
|
|
2620
|
+
console.log(` ${chalk9.yellow(commit.revision.slice(0, 10))} ${commit.message.split("\n")[0]}`);
|
|
2621
|
+
console.log(chalk9.gray(` ${commit.author} \u2014 ${commit.date}`));
|
|
2622
|
+
}
|
|
2623
|
+
for (const changed of result.commit?.changedFiles ?? []) {
|
|
2624
|
+
console.log(chalk9.gray(` ${changed.change.padEnd(8)} ${changed.path}`));
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
if (result.truncated) {
|
|
2628
|
+
console.log(chalk9.yellow(" The list was capped \u2014 raise --limit to see more."));
|
|
2629
|
+
}
|
|
2630
|
+
if (result.revision) {
|
|
2631
|
+
console.log();
|
|
2632
|
+
console.log(chalk9.gray(` revision: ${result.revision} (pass to \`anima edit --base-revision\`)`));
|
|
2633
|
+
}
|
|
2634
|
+
console.log();
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// src/commands/edit.ts
|
|
2638
|
+
import { Command as Command16 } from "commander";
|
|
2639
|
+
import fs5 from "fs";
|
|
2640
|
+
import chalk10 from "chalk";
|
|
2641
|
+
var editCommand = new Command16("edit").summary("Change an artifact's files and commit them").description(
|
|
2642
|
+
"Change an artifact's files and commit them, without git. All changes land as one commit."
|
|
2643
|
+
).argument("<artifactUrl>", "Artifact URL or session ID").requiredOption("-m, --message <message>", "Commit message").option(
|
|
2644
|
+
"--base-revision <rev>",
|
|
2645
|
+
"The revision the edit applies to (from `anima explore`). Defaults to the artifact's current head"
|
|
2646
|
+
).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) => {
|
|
2647
|
+
const mode = detectOutputMode(opts);
|
|
2648
|
+
initLogger(opts.logFile);
|
|
2649
|
+
await withCommandErrors(mode, async () => {
|
|
2650
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
2651
|
+
const changes = buildChanges(opts);
|
|
2652
|
+
const baseRevision = opts.baseRevision ?? await currentRevision(sessionId, opts.apiUrl);
|
|
2653
|
+
const result = await runTool(
|
|
2654
|
+
"artifact-edit",
|
|
2655
|
+
{ sessionId, baseRevision, commitMessage: opts.message, changes },
|
|
2656
|
+
{ apiUrl: opts.apiUrl }
|
|
2657
|
+
);
|
|
2658
|
+
if (mode === "pretty") {
|
|
2659
|
+
console.log();
|
|
2660
|
+
printSuccess("Committed.");
|
|
2661
|
+
for (const changed of result.changedFiles ?? []) {
|
|
2662
|
+
const detail = changed.op === "move" ? `${changed.fromPath} \u2192 ${changed.path}` : changed.replacements !== void 0 ? `${changed.path} (${changed.replacements} replaced)` : changed.path;
|
|
2663
|
+
console.log(` ${chalk10.gray(changed.op.padEnd(12))} ${detail}`);
|
|
2664
|
+
}
|
|
2665
|
+
console.log();
|
|
2666
|
+
printKeyValue("Revision", result.revision ?? "unknown");
|
|
2667
|
+
console.log();
|
|
2668
|
+
console.log(" The live artifact reflects this commit.");
|
|
2669
|
+
console.log();
|
|
2670
|
+
} else {
|
|
2671
|
+
outputResult(mode, result);
|
|
2672
|
+
}
|
|
2673
|
+
});
|
|
2674
|
+
});
|
|
2675
|
+
async function currentRevision(sessionId, apiUrl) {
|
|
2676
|
+
const result = await runTool(
|
|
2677
|
+
"artifact-explore",
|
|
2678
|
+
{ sessionId, action: "tree", limit: 1 },
|
|
2679
|
+
{ apiUrl }
|
|
2680
|
+
);
|
|
2681
|
+
if (!result.revision) {
|
|
2682
|
+
throw validationError(
|
|
2683
|
+
"Could not read the artifact's current revision. Pass --base-revision from `anima explore`."
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2686
|
+
return result.revision;
|
|
2687
|
+
}
|
|
2688
|
+
function buildChanges(opts) {
|
|
2689
|
+
const explicit = opts.changes ?? opts.changesFile;
|
|
2690
|
+
const shorthands = [opts.write, opts.delete, opts.move, opts.replace].filter(Boolean);
|
|
2691
|
+
if (explicit && shorthands.length) {
|
|
2692
|
+
throw validationError(
|
|
2693
|
+
"--changes/--changes-file already carries the whole change list; drop the --write/--delete/--move/--replace shorthand."
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
if (explicit) {
|
|
2697
|
+
const raw = opts.changesFile ? readFile(opts.changesFile) : opts.changes;
|
|
2698
|
+
return parseChanges(raw);
|
|
2699
|
+
}
|
|
2700
|
+
if (shorthands.length !== 1) {
|
|
2701
|
+
throw validationError(
|
|
2702
|
+
"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>."
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
if (opts.write) {
|
|
2706
|
+
if (opts.content === void 0 && !opts.fromFile) {
|
|
2707
|
+
throw validationError("--write needs the file's content: pass --content <text> or --from-file <path>.");
|
|
2708
|
+
}
|
|
2709
|
+
if (opts.content !== void 0 && opts.fromFile) {
|
|
2710
|
+
throw validationError("Pass either --content or --from-file for --write, not both.");
|
|
2711
|
+
}
|
|
2712
|
+
return [
|
|
2713
|
+
{
|
|
2714
|
+
op: "write",
|
|
2715
|
+
path: opts.write,
|
|
2716
|
+
content: opts.fromFile ? readFile(opts.fromFile) : opts.content
|
|
2717
|
+
}
|
|
2718
|
+
];
|
|
2719
|
+
}
|
|
2720
|
+
if (opts.delete) {
|
|
2721
|
+
return [{ op: "delete", path: opts.delete }];
|
|
2722
|
+
}
|
|
2723
|
+
if (opts.move) {
|
|
2724
|
+
if (!opts.to) throw validationError("--move needs --to <path>, the new path.");
|
|
2725
|
+
return [{ op: "move", fromPath: opts.move, toPath: opts.to }];
|
|
2726
|
+
}
|
|
2727
|
+
if (opts.old === void 0 || opts.new === void 0) {
|
|
2728
|
+
throw validationError('--replace needs --old <text> and --new <text>. Pass --new "" to delete the snippet.');
|
|
2729
|
+
}
|
|
2730
|
+
return [
|
|
2731
|
+
{
|
|
2732
|
+
op: "str_replace",
|
|
2733
|
+
path: opts.replace,
|
|
2734
|
+
oldText: opts.old,
|
|
2735
|
+
newText: opts.new,
|
|
2736
|
+
...opts.all ? { replaceAll: true } : {}
|
|
2737
|
+
}
|
|
2738
|
+
];
|
|
2739
|
+
}
|
|
2740
|
+
function parseChanges(raw) {
|
|
2741
|
+
let parsed;
|
|
2742
|
+
try {
|
|
2743
|
+
parsed = JSON.parse(raw);
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
throw validationError(`--changes is not valid JSON: ${err.message}`);
|
|
2746
|
+
}
|
|
2747
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
2748
|
+
throw validationError("--changes must be a non-empty JSON array of change objects.");
|
|
2749
|
+
}
|
|
2750
|
+
return parsed;
|
|
2751
|
+
}
|
|
2752
|
+
function readFile(path7) {
|
|
2753
|
+
try {
|
|
2754
|
+
return fs5.readFileSync(path7, "utf-8");
|
|
2755
|
+
} catch (err) {
|
|
2756
|
+
throw validationError(`Could not read ${path7}: ${err.message}`);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
// src/commands/uploadAsset.ts
|
|
2761
|
+
import { Command as Command17 } from "commander";
|
|
2762
|
+
import fs6 from "fs";
|
|
2763
|
+
import path5 from "path";
|
|
2764
|
+
import chalk11 from "chalk";
|
|
2765
|
+
var uploadAssetCommand = new Command17("upload-asset").summary("Add an image, font or media file over 256 KB").description(
|
|
2766
|
+
"Add a large image, font or media file (over 256 KB) to an artifact: stage it, upload it, and commit it at --path."
|
|
2767
|
+
).argument("<artifactUrl>", "Artifact URL or session ID").argument("<file>", "Local file to upload").option(
|
|
2768
|
+
"--path <path>",
|
|
2769
|
+
"Where the asset lands in the artifact (e.g. public/hero.png). Omit to stage only and print the assetUploadId"
|
|
2770
|
+
).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) => {
|
|
2771
|
+
const mode = detectOutputMode(opts);
|
|
2772
|
+
initLogger(opts.logFile);
|
|
2773
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
2774
|
+
await withCommandErrors(mode, async () => {
|
|
2775
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
2776
|
+
const bytes = readAsset(file);
|
|
2777
|
+
progress.start("Staging the upload...");
|
|
2778
|
+
const grant = await runTool(
|
|
2779
|
+
"artifact-get_asset_upload_url",
|
|
2780
|
+
{ sessionId, filename: path5.basename(file) },
|
|
2781
|
+
{ apiUrl: opts.apiUrl }
|
|
2782
|
+
);
|
|
2783
|
+
if (!grant.uploadUrl || !grant.uploadFields || !grant.assetUploadId) {
|
|
2784
|
+
throw apiError("The staging response carried no upload URL.", grant);
|
|
2785
|
+
}
|
|
2786
|
+
if (grant.maxBytes !== void 0 && bytes.byteLength > grant.maxBytes) {
|
|
2787
|
+
throw validationError(
|
|
2788
|
+
`${file} is ${bytes.byteLength} bytes; the limit is ${grant.maxBytes}. Push it with git instead: anima get-git-token ${sessionId}`
|
|
2789
|
+
);
|
|
2790
|
+
}
|
|
2791
|
+
progress.update("Uploading...");
|
|
2792
|
+
await postAsset(grant.uploadUrl, grant.uploadFields, bytes, path5.basename(file));
|
|
2793
|
+
logEvent("asset_uploaded", { sessionId, assetUploadId: grant.assetUploadId });
|
|
2794
|
+
if (!opts.path) {
|
|
2795
|
+
progress.succeed("Uploaded \u2014 not committed yet.");
|
|
2796
|
+
if (mode === "pretty") {
|
|
2797
|
+
console.log();
|
|
2798
|
+
printKeyValue("assetUploadId", grant.assetUploadId);
|
|
2799
|
+
console.log();
|
|
2800
|
+
console.log(" Commit it where it belongs with:");
|
|
2801
|
+
console.log(
|
|
2802
|
+
` ${chalk11.cyan(
|
|
2803
|
+
`anima edit ${sessionId} -m "Add asset" --changes '[{"op":"write","path":"public/${path5.basename(file)}","assetUploadId":"${grant.assetUploadId}"}]'`
|
|
2804
|
+
)}`
|
|
2805
|
+
);
|
|
2806
|
+
console.log();
|
|
2807
|
+
} else {
|
|
2808
|
+
outputResult(mode, {
|
|
2809
|
+
success: true,
|
|
2810
|
+
sessionId,
|
|
2811
|
+
assetUploadId: grant.assetUploadId,
|
|
2812
|
+
expiresAt: grant.expiresAt,
|
|
2813
|
+
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."
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
progress.update("Committing...");
|
|
2819
|
+
const baseRevision = opts.baseRevision ?? await currentRevision2(sessionId, opts.apiUrl);
|
|
2820
|
+
const result = await runTool(
|
|
2821
|
+
"artifact-edit",
|
|
2822
|
+
{
|
|
2823
|
+
sessionId,
|
|
2824
|
+
baseRevision,
|
|
2825
|
+
commitMessage: opts.message ?? `Add ${opts.path}`,
|
|
2826
|
+
changes: [{ op: "write", path: opts.path, assetUploadId: grant.assetUploadId }]
|
|
2827
|
+
},
|
|
2828
|
+
{ apiUrl: opts.apiUrl }
|
|
2829
|
+
);
|
|
2830
|
+
progress.succeed("Uploaded and committed!");
|
|
2831
|
+
if (mode === "pretty") {
|
|
2832
|
+
console.log();
|
|
2833
|
+
printSuccess(`${file} \u2192 ${opts.path}`);
|
|
2834
|
+
printKeyValue("Revision", result.revision ?? "unknown");
|
|
2835
|
+
console.log();
|
|
2836
|
+
} else {
|
|
2837
|
+
outputResult(mode, {
|
|
2838
|
+
success: true,
|
|
2839
|
+
sessionId,
|
|
2840
|
+
path: opts.path,
|
|
2841
|
+
assetUploadId: grant.assetUploadId,
|
|
2842
|
+
revision: result.revision
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
});
|
|
2847
|
+
async function postAsset(uploadUrl, uploadFields, bytes, filename) {
|
|
2848
|
+
const form = new FormData();
|
|
2849
|
+
for (const [key, value] of Object.entries(uploadFields)) {
|
|
2850
|
+
form.append(key, value);
|
|
2851
|
+
}
|
|
2852
|
+
form.append("file", new Blob([new Uint8Array(bytes)]), filename);
|
|
2853
|
+
const response = await fetch(uploadUrl, { method: "POST", body: form });
|
|
2854
|
+
if (!response.ok) {
|
|
2855
|
+
const detail = await response.text().catch(() => "");
|
|
2856
|
+
throw apiError(`Asset upload failed: HTTP ${response.status}${detail ? ` \u2014 ${detail.slice(0, 300)}` : ""}`);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
async function currentRevision2(sessionId, apiUrl) {
|
|
2860
|
+
const result = await runTool(
|
|
2861
|
+
"artifact-explore",
|
|
2862
|
+
{ sessionId, action: "tree", limit: 1 },
|
|
2863
|
+
{ apiUrl }
|
|
2864
|
+
);
|
|
2865
|
+
if (!result.revision) {
|
|
2866
|
+
throw validationError("Could not read the artifact's current revision. Pass --base-revision.");
|
|
2867
|
+
}
|
|
2868
|
+
return result.revision;
|
|
2869
|
+
}
|
|
2870
|
+
function readAsset(file) {
|
|
2871
|
+
try {
|
|
2872
|
+
return fs6.readFileSync(file);
|
|
2873
|
+
} catch (err) {
|
|
2874
|
+
throw validationError(`Could not read ${file}: ${err.message}`);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// src/commands/delete.ts
|
|
2879
|
+
import { Command as Command18 } from "commander";
|
|
2880
|
+
var deleteCommand = new Command18("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) => {
|
|
2881
|
+
const mode = detectOutputMode(opts);
|
|
2882
|
+
initLogger(opts.logFile);
|
|
2883
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
2884
|
+
try {
|
|
2885
|
+
const sessionId = parseArtifactUrl(artifactUrl);
|
|
2886
|
+
if (!sessionId) {
|
|
2887
|
+
throw validationError("An artifact URL or session ID is required");
|
|
2888
|
+
}
|
|
2889
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
2890
|
+
progress.start("Deleting artifact...");
|
|
2891
|
+
const result = await withMcp(
|
|
2892
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
2893
|
+
(client) => client.callTool("artifact-delete", { sessionId })
|
|
2894
|
+
);
|
|
2895
|
+
if (!result.success) {
|
|
2896
|
+
throw apiError(result.message || result.reason || result.error || "Artifact deletion failed", result);
|
|
2897
|
+
}
|
|
2898
|
+
progress.succeed("Artifact deleted");
|
|
2899
|
+
if (mode === "pretty") {
|
|
2900
|
+
printSuccess(result.message || `Artifact ${sessionId} is hidden and can be restored.`);
|
|
2901
|
+
} else {
|
|
2902
|
+
outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
|
|
2903
|
+
}
|
|
2904
|
+
} catch (err) {
|
|
2905
|
+
progress.fail("Failed");
|
|
2906
|
+
if (err instanceof CliError) {
|
|
2907
|
+
const detail = err.detail;
|
|
2908
|
+
const code = typeof detail?.code === "string" ? detail.code : err.code;
|
|
2909
|
+
if (mode === "pretty") {
|
|
2910
|
+
printError(code === err.code ? err.message : `${code}: ${err.message}`);
|
|
2911
|
+
} else {
|
|
2912
|
+
outputError(mode, { code, message: err.message });
|
|
2913
|
+
}
|
|
2914
|
+
process.exit(err.exitCode);
|
|
2915
|
+
}
|
|
2916
|
+
throw err;
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2919
|
+
|
|
2920
|
+
// src/commands/review.ts
|
|
2921
|
+
import { Command as Command19 } from "commander";
|
|
2922
|
+
import chalk12 from "chalk";
|
|
2923
|
+
var listSubcommand = new Command19("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) => {
|
|
2924
|
+
const mode = detectOutputMode(opts);
|
|
2925
|
+
initLogger(opts.logFile);
|
|
2926
|
+
await withCommandErrors(mode, async () => {
|
|
2927
|
+
const args = {};
|
|
2928
|
+
if (artifactUrl) args.sessionId = parseArtifactUrl(artifactUrl);
|
|
2929
|
+
if (opts.limit !== void 0) args.limit = parseLimit2(opts.limit);
|
|
2930
|
+
const result = await runTool("review-list", args, { apiUrl: opts.apiUrl });
|
|
2931
|
+
if (mode === "pretty") {
|
|
2932
|
+
printReviews(result);
|
|
2933
|
+
} else {
|
|
2934
|
+
outputResult(mode, result);
|
|
2935
|
+
}
|
|
2936
|
+
});
|
|
2937
|
+
});
|
|
2938
|
+
var resolveSubcommand = new Command19("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) => {
|
|
2939
|
+
const mode = detectOutputMode(opts);
|
|
2940
|
+
initLogger(opts.logFile);
|
|
2941
|
+
await withCommandErrors(mode, async () => {
|
|
2942
|
+
const result = await runTool(
|
|
2943
|
+
"review-resolve",
|
|
2944
|
+
{ commentId, ...opts.note ? { note: opts.note } : {} },
|
|
2945
|
+
{ apiUrl: opts.apiUrl }
|
|
2946
|
+
);
|
|
2947
|
+
if (mode === "pretty") {
|
|
2948
|
+
console.log();
|
|
2949
|
+
if (result.resolved) printSuccess(result.message ?? "Comment resolved.");
|
|
2950
|
+
else printInfo(result.message ?? "Comment was already closed.");
|
|
2951
|
+
console.log();
|
|
2952
|
+
} else {
|
|
2953
|
+
outputResult(mode, result);
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
});
|
|
2957
|
+
var replySubcommand = new Command19("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) => {
|
|
2958
|
+
const mode = detectOutputMode(opts);
|
|
2959
|
+
initLogger(opts.logFile);
|
|
2960
|
+
await withCommandErrors(mode, async () => {
|
|
2961
|
+
const result = await runTool(
|
|
2962
|
+
"review-reply",
|
|
2963
|
+
{ commentId, message: opts.message },
|
|
2964
|
+
{ apiUrl: opts.apiUrl }
|
|
2965
|
+
);
|
|
2966
|
+
if (mode === "pretty") {
|
|
2967
|
+
console.log();
|
|
2968
|
+
if (result.replied) printSuccess(result.message ?? "Reply posted.");
|
|
2969
|
+
else printInfo(result.message ?? "The comment is closed, so nothing was posted.");
|
|
2970
|
+
console.log();
|
|
2971
|
+
} else {
|
|
2972
|
+
outputResult(mode, result);
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
});
|
|
2976
|
+
var reviewCommand = new Command19("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);
|
|
2977
|
+
function parseLimit2(value) {
|
|
2978
|
+
const limit = Number(value);
|
|
2979
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
2980
|
+
throw validationError(`--limit must be a whole number between 1 and 50 (got "${value}").`);
|
|
2981
|
+
}
|
|
2982
|
+
return limit;
|
|
2983
|
+
}
|
|
2984
|
+
function printReviews(result) {
|
|
2985
|
+
const reviews = result.reviews ?? [];
|
|
2986
|
+
console.log();
|
|
2987
|
+
if (reviews.length === 0) {
|
|
2988
|
+
console.log(` ${result.message ?? "No open reviews are addressed to you."}`);
|
|
2989
|
+
console.log();
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
for (const review of reviews) {
|
|
2993
|
+
console.log(` ${chalk12.bold(`Review ${review.reviewId}`)} ${chalk12.gray(`by ${review.author.name}`)}`);
|
|
2994
|
+
console.log(` ${chalk12.cyan.underline(review.artifactUrl)}`);
|
|
2995
|
+
if (!review.isLatest) {
|
|
2996
|
+
console.log(
|
|
2997
|
+
chalk12.yellow(" The artifact moved since this was written \u2014 check the comments still apply.")
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
console.log();
|
|
3001
|
+
for (const comment of review.comments) {
|
|
3002
|
+
const marks = [
|
|
3003
|
+
comment.status !== "open" ? chalk12.gray(comment.status) : null,
|
|
3004
|
+
comment.unassigned ? chalk12.yellow("unassigned") : null
|
|
3005
|
+
].filter(Boolean).join(" ");
|
|
3006
|
+
console.log(` ${chalk12.bold(`#${comment.index}`)} ${comment.body}${marks ? ` ${marks}` : ""}`);
|
|
3007
|
+
console.log(chalk12.gray(` ${comment.commentId}${describeAnchor(comment)}`));
|
|
3008
|
+
for (const reply of comment.replies ?? []) {
|
|
3009
|
+
console.log(chalk12.gray(` \u21B3 ${reply.author.name}: ${reply.body}`));
|
|
3010
|
+
}
|
|
3011
|
+
console.log();
|
|
3012
|
+
}
|
|
3013
|
+
printKeyValue("To close these in a commit", "");
|
|
3014
|
+
for (const line of review.resolveTrailer.split("\n")) {
|
|
3015
|
+
console.log(` ${chalk12.gray(line)}`);
|
|
3016
|
+
}
|
|
3017
|
+
console.log();
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
function describeAnchor(comment) {
|
|
3021
|
+
if (!comment.anchor) return " \u2014 about the artifact as a whole";
|
|
3022
|
+
const anchor = comment.anchor;
|
|
3023
|
+
const dom = anchor.dom;
|
|
3024
|
+
const markdown = anchor.markdown;
|
|
3025
|
+
if (dom?.sourceFile) {
|
|
3026
|
+
const instances = typeof dom.instanceCount === "number" && dom.instanceCount > 1 ? dom.instanceCount : 0;
|
|
3027
|
+
return ` \u2014 ${dom.sourceFile}${dom.sourceStart ? `:${dom.sourceStart}` : ""}${instances ? ` (shared markup: ${instances} instances)` : ""}`;
|
|
3028
|
+
}
|
|
3029
|
+
if (markdown?.quote) {
|
|
3030
|
+
return ` \u2014 on "${String(markdown.quote).slice(0, 60)}"`;
|
|
3031
|
+
}
|
|
3032
|
+
return "";
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
// src/commands/skill.ts
|
|
3036
|
+
import { Command as Command20 } from "commander";
|
|
3037
|
+
import fs7 from "fs";
|
|
3038
|
+
import path6 from "path";
|
|
3039
|
+
import chalk13 from "chalk";
|
|
3040
|
+
var GUIDE_PATH = "/guide.md";
|
|
3041
|
+
function createSkillCommand() {
|
|
3042
|
+
return new Command20("skill").summary("Install the AgentGrid guide as a skill your agent can read").description(
|
|
3043
|
+
"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."
|
|
3044
|
+
).argument(
|
|
3045
|
+
"[path]",
|
|
3046
|
+
"Where to write the SKILL.md (e.g. .claude/skills/agentgrid/SKILL.md). Omit to print to stdout"
|
|
3047
|
+
).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) => {
|
|
3048
|
+
const mode = detectOutputMode(opts);
|
|
3049
|
+
initLogger(opts.logFile);
|
|
3050
|
+
await withCommandErrors(mode, async () => {
|
|
3051
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
3052
|
+
const guide = await fetchGuide(apiUrl);
|
|
3053
|
+
const program2 = command.parent ?? command;
|
|
3054
|
+
const body = [connectSection(), commandReference(program2), guide.trim(), provenance(apiUrl)].join(
|
|
3055
|
+
"\n\n"
|
|
3056
|
+
);
|
|
3057
|
+
const document = `${frontmatter()}
|
|
3058
|
+
${body}
|
|
3059
|
+
`;
|
|
3060
|
+
if (!target) {
|
|
3061
|
+
process.stdout.write(document.endsWith("\n") ? document : `${document}
|
|
3062
|
+
`);
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
writeDocument(target, document);
|
|
3066
|
+
logEvent("skill_installed", { path: target, bytes: document.length });
|
|
3067
|
+
if (mode === "pretty") {
|
|
3068
|
+
console.log();
|
|
3069
|
+
printSuccess(`Skill written to ${chalk13.cyan(target)}`);
|
|
3070
|
+
console.log(` ${chalk13.gray(`${document.length} bytes, from ${apiUrl}${GUIDE_PATH}`)}`);
|
|
3071
|
+
console.log();
|
|
3072
|
+
} else {
|
|
3073
|
+
outputResult(mode, {
|
|
3074
|
+
success: true,
|
|
3075
|
+
path: path6.resolve(target),
|
|
3076
|
+
bytes: document.length,
|
|
3077
|
+
source: `${apiUrl}${GUIDE_PATH}`
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
});
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
var skillCommand = createSkillCommand();
|
|
3084
|
+
async function fetchGuide(apiUrl) {
|
|
3085
|
+
const url = `${apiUrl.replace(/\/$/, "")}${GUIDE_PATH}`;
|
|
3086
|
+
logEvent("skill_fetch", { url });
|
|
3087
|
+
let response;
|
|
3088
|
+
try {
|
|
3089
|
+
response = await fetch(url);
|
|
3090
|
+
} catch (err) {
|
|
3091
|
+
throw apiError(`Could not reach ${url}: ${err.message}`);
|
|
3092
|
+
}
|
|
3093
|
+
if (response.status === 404) {
|
|
3094
|
+
throw apiError(
|
|
3095
|
+
`${url} returned 404. This API does not serve the agent guide yet \u2014 point --api-url at one that does.`
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
if (!response.ok) {
|
|
3099
|
+
throw apiError(`Could not fetch the guide: HTTP ${response.status} from ${url}`);
|
|
3100
|
+
}
|
|
3101
|
+
const body = await response.text();
|
|
3102
|
+
if (!body.trim()) {
|
|
3103
|
+
throw apiError(`${url} returned an empty guide.`);
|
|
3104
|
+
}
|
|
3105
|
+
return body;
|
|
3106
|
+
}
|
|
3107
|
+
function frontmatter() {
|
|
3108
|
+
return [
|
|
3109
|
+
"---",
|
|
3110
|
+
"name: agentgrid",
|
|
3111
|
+
"description: >-",
|
|
3112
|
+
" Host, change, publish and share artifacts on AgentGrid from the command line.",
|
|
3113
|
+
" Use when asked to share an app or a document with a human, put work somewhere",
|
|
3114
|
+
" they can see it, create or edit an AgentGrid artifact, publish one to a live",
|
|
3115
|
+
" URL, or answer review comments left on one.",
|
|
3116
|
+
"---"
|
|
3117
|
+
].join("\n");
|
|
3118
|
+
}
|
|
3119
|
+
function connectSection() {
|
|
3120
|
+
return `# AgentGrid
|
|
3121
|
+
|
|
3122
|
+
An **artifact** is a hosted app or document that is also a real git repository.
|
|
3123
|
+
Every command below runs one AgentGrid tool for you.
|
|
3124
|
+
|
|
3125
|
+
## Connect first
|
|
3126
|
+
|
|
3127
|
+
You act as a scoped agent identity a human approved and can revoke. Three doors \u2014
|
|
3128
|
+
the first is the usual one, because you arrive with something to show and they
|
|
3129
|
+
need no account until they claim it:
|
|
3130
|
+
|
|
3131
|
+
\`\`\`bash
|
|
3132
|
+
# 1. You lead: create with no credential at all, then wait for the claim
|
|
3133
|
+
npx @animaapp/cli@latest create --anonymous -t import --from ./my-project \\
|
|
3134
|
+
--client-name "<your product name>" --json # returns artifactUrl \u2014 send it to your human
|
|
3135
|
+
npx @animaapp/cli@latest login --handoff --json # blocks until they claim; stores your credentials
|
|
3136
|
+
|
|
3137
|
+
# 2. A human is reachable and can approve a short code
|
|
3138
|
+
npx @animaapp/cli@latest login
|
|
3139
|
+
|
|
3140
|
+
# 3. A human already sent you an invite link
|
|
3141
|
+
npx @animaapp/cli@latest login --invite <url-or-code>
|
|
3142
|
+
\`\`\`
|
|
3143
|
+
|
|
3144
|
+
Credentials live in \`~/.config/anima/\` and the CLI renews them for you. Check
|
|
3145
|
+
with \`anima auth --status\`. Every command takes \`--json\` for machine-readable
|
|
3146
|
+
output on stdout, with progress on stderr.`;
|
|
3147
|
+
}
|
|
3148
|
+
function commandReference(program2) {
|
|
3149
|
+
const rows = [];
|
|
3150
|
+
for (const command of program2.commands) {
|
|
3151
|
+
if (command.name() === "help") continue;
|
|
3152
|
+
const subcommands = command.commands.filter((sub) => sub.name() !== "help");
|
|
3153
|
+
if (subcommands.length > 0) {
|
|
3154
|
+
for (const sub of subcommands) {
|
|
3155
|
+
rows.push(row(`${command.name()} ${sub.name()}`, sub));
|
|
3156
|
+
}
|
|
3157
|
+
continue;
|
|
3158
|
+
}
|
|
3159
|
+
rows.push(row(command.name(), command));
|
|
3160
|
+
}
|
|
3161
|
+
return [
|
|
3162
|
+
"## Command reference",
|
|
3163
|
+
"",
|
|
3164
|
+
`Generated from \`@animaapp/cli\` ${version}, so this is what your copy accepts.`,
|
|
3165
|
+
"Run `anima <command> --help` for a command's full options.",
|
|
3166
|
+
"",
|
|
3167
|
+
"| Command | What it does |",
|
|
3168
|
+
"| ------- | ------------ |",
|
|
3169
|
+
...rows
|
|
3170
|
+
].join("\n");
|
|
3171
|
+
}
|
|
3172
|
+
function row(name, command) {
|
|
3173
|
+
const usage = command.usage().replace(/\[options\]\s*/, "").trim();
|
|
3174
|
+
const invocation = `anima ${name}${usage ? ` ${usage}` : ""}`;
|
|
3175
|
+
const summary = (command.summary() || command.description() || "").split("\n")[0];
|
|
3176
|
+
return `| \`${invocation}\` | ${escapePipes(summary)} |`;
|
|
3177
|
+
}
|
|
3178
|
+
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
3179
|
+
function provenance(apiUrl) {
|
|
3180
|
+
return `---
|
|
3181
|
+
|
|
3182
|
+
_Guide fetched from ${apiUrl}${GUIDE_PATH}; command reference generated by @animaapp/cli ${version}._`;
|
|
3183
|
+
}
|
|
3184
|
+
function writeDocument(target, document) {
|
|
3185
|
+
const directory = path6.dirname(path6.resolve(target));
|
|
3186
|
+
try {
|
|
3187
|
+
fs7.mkdirSync(directory, { recursive: true });
|
|
3188
|
+
} catch (err) {
|
|
3189
|
+
throw validationError(`Could not create ${directory}: ${err.message}`);
|
|
3190
|
+
}
|
|
3191
|
+
try {
|
|
3192
|
+
fs7.writeFileSync(target, document, "utf-8");
|
|
3193
|
+
} catch (err) {
|
|
3194
|
+
throw validationError(`Could not write ${target}: ${err.message}`);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
|
|
2263
3198
|
// src/index.ts
|
|
2264
|
-
var program = new
|
|
2265
|
-
`AgentGrid CLI \u2014
|
|
2266
|
-
real git repository
|
|
3199
|
+
var program = new Command21().name("anima").description(
|
|
3200
|
+
`AgentGrid CLI \u2014 create, change, publish and share artifacts on AgentGrid.
|
|
3201
|
+
An artifact is a real git repository, and every command below runs one
|
|
3202
|
+
AgentGrid MCP tool for you. The command list is at the bottom; run
|
|
3203
|
+
"anima <command> --help" for a command's own options.
|
|
2267
3204
|
|
|
2268
3205
|
THE MAIN FLOW (build something and put it on AgentGrid):
|
|
2269
|
-
1.
|
|
2270
|
-
2.
|
|
3206
|
+
1. anima login # once; a human approves
|
|
3207
|
+
2. anima create -t import --from ./my-project # your code \u2192 a live app
|
|
2271
3208
|
(or: create -t empty --framework html \u2014 an empty repo you push to)
|
|
2272
|
-
3.
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
3209
|
+
3. Change it. Two doors, same git repository:
|
|
3210
|
+
- no clone: "explore" to read the files, "edit" to commit them. Needs no
|
|
3211
|
+
shell, no git binary and no network of its own \u2014 prefer it.
|
|
3212
|
+
- a real checkout: "get-git-token" (create already returns one), then
|
|
3213
|
+
git clone / commit / push. Worth it for a large refactor, running the
|
|
3214
|
+
project, branches or rewriting history.
|
|
3215
|
+
4. Share the artifactUrl \u2014 app.agentgrid.io/artifacts/<sessionId>. That IS
|
|
3216
|
+
sharing: the artifact is already visible there. Do NOT run "publish"
|
|
3217
|
+
unless a human explicitly asked for a public site; offer it as a
|
|
3218
|
+
follow-up question instead.
|
|
3219
|
+
|
|
3220
|
+
TIMING \u2014 how long the COMMAND blocks, not how long the server takes:
|
|
3221
|
+
create -t p2c|l2c|f2c BLOCKS 3-7 min. The generation itself is
|
|
3222
|
+
asynchronous, but create waits it out for you, so
|
|
3223
|
+
what it prints is a finished app and a failed
|
|
3224
|
+
generation exits non-zero. Keep the default
|
|
3225
|
+
600000ms --timeout. Pass --no-wait to get the
|
|
3226
|
+
sessionId back in seconds and do the waiting
|
|
3227
|
+
yourself with "anima status <sessionId> --wait".
|
|
3228
|
+
publish / unpublish BLOCKS 1-3 min
|
|
3229
|
+
everything else seconds, create -t empty|import included
|
|
2277
3230
|
|
|
2278
3231
|
AUTHENTICATION:
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
update Rename an artifact or set visibility public/private (metadata only)
|
|
2297
|
-
publish Deploy to a public live URL \u2014 ONLY when the human asked for it
|
|
2298
|
-
unpublish Take a published artifact offline (inverse of publish)
|
|
2299
|
-
mcp-config Print an MCP server config from stored credentials (no new login)
|
|
2300
|
-
codegen Convert Figma design to local code files (no artifact)
|
|
2301
|
-
config Store CLI preferences (e.g. a default API URL)
|
|
2302
|
-
auth Inspect or manage credentials (status / figma token)
|
|
2303
|
-
|
|
2304
|
-
EXAMPLES (headless flow):
|
|
2305
|
-
npx @animaapp/cli login --invite https://api.agentgrid.io/invite/<code>.md
|
|
2306
|
-
npx @animaapp/cli create -t import --from ./my-project
|
|
2307
|
-
npx @animaapp/cli create -t empty --framework react --name "My app"
|
|
2308
|
-
npx @animaapp/cli duplicate <artifactUrl> --name "My copy"
|
|
2309
|
-
npx @animaapp/cli get-git-token <sessionId>
|
|
2310
|
-
npx @animaapp/cli update <sessionId> --privacy public
|
|
2311
|
-
npx @animaapp/cli publish <sessionId> # only on explicit request
|
|
2312
|
-
npx @animaapp/cli unpublish <sessionId>
|
|
2313
|
-
|
|
2314
|
-
OUTPUT: JSON to stdout when piped (for AI tools), pretty text in terminals.
|
|
2315
|
-
Progress is streamed to stderr via --verbose when in JSON mode.`
|
|
3232
|
+
anima skill <path> # install this guide where your runtime reads skills
|
|
3233
|
+
anima login # device flow \u2014 a human approves on any device
|
|
3234
|
+
anima login --invite <url> # redeem a pre-approved invite (no approval step)
|
|
3235
|
+
anima create --anonymous \u2026 # no account at all; hand off to a human later
|
|
3236
|
+
Credentials live in ~/.config/anima/; "logout" removes everything.
|
|
3237
|
+
|
|
3238
|
+
EXAMPLES:
|
|
3239
|
+
anima create -t import --from ./my-project
|
|
3240
|
+
anima status <sessionId> --wait
|
|
3241
|
+
anima explore <sessionId> --search "buttonColor"
|
|
3242
|
+
anima edit <sessionId> -m "Blue pill" --replace src/App.tsx --old red --new blue
|
|
3243
|
+
anima review list # what humans asked you to change
|
|
3244
|
+
anima publish <sessionId> # only on explicit request
|
|
3245
|
+
|
|
3246
|
+
OUTPUT: pretty text in a terminal; a single JSON object on stdout when piped
|
|
3247
|
+
or given --json. Progress and prompts go to stderr, so stdout stays clean.
|
|
3248
|
+
Add --verbose to stream progress in JSON mode.`
|
|
2316
3249
|
).version(version, "-v, --version");
|
|
2317
3250
|
program.addCommand(loginCommand);
|
|
2318
3251
|
program.addCommand(logoutCommand);
|
|
2319
3252
|
program.addCommand(listCommand);
|
|
2320
3253
|
program.addCommand(createCommand);
|
|
3254
|
+
program.addCommand(statusCommand);
|
|
3255
|
+
program.addCommand(exploreCommand);
|
|
3256
|
+
program.addCommand(editCommand);
|
|
3257
|
+
program.addCommand(uploadAssetCommand);
|
|
3258
|
+
program.addCommand(getGitTokenCommand);
|
|
3259
|
+
program.addCommand(reviewCommand);
|
|
2321
3260
|
program.addCommand(duplicateCommand);
|
|
2322
3261
|
program.addCommand(publishCommand);
|
|
2323
3262
|
program.addCommand(unpublishCommand);
|
|
2324
3263
|
program.addCommand(updateCommand);
|
|
3264
|
+
program.addCommand(deleteCommand);
|
|
2325
3265
|
program.addCommand(codegenCommand);
|
|
2326
|
-
program.addCommand(
|
|
3266
|
+
program.addCommand(skillCommand);
|
|
2327
3267
|
program.addCommand(mcpConfigCommand);
|
|
2328
3268
|
program.addCommand(configCommand);
|
|
2329
3269
|
program.addCommand(authCommand);
|