@impractical-ai/motion 0.1.3 → 0.1.4

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.
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ PRODUCTION_APP_URL,
4
+ PRODUCTION_SUPABASE_URL
5
+ } from "./chunk-EFP4P3WA.js";
6
+
7
+ // src/config.ts
8
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync, renameSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ var configDir = () => join(homedir(), ".impractical");
12
+ var configPath = () => join(configDir(), "config.json");
13
+ function recoverCorruptConfig() {
14
+ const path = configPath();
15
+ if (!existsSync(path)) return null;
16
+ try {
17
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
18
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
19
+ throw new SyntaxError("the root value must be an object");
20
+ }
21
+ return null;
22
+ } catch (error) {
23
+ if (!(error instanceof SyntaxError)) {
24
+ throw new Error(
25
+ `Could not inspect ${path}: ${error instanceof Error ? error.message : String(error)}`
26
+ );
27
+ }
28
+ let backupPath = `${path}.corrupt-${Date.now()}`;
29
+ let suffix = 1;
30
+ while (existsSync(backupPath)) backupPath = `${path}.corrupt-${Date.now()}-${suffix++}`;
31
+ renameSync(path, backupPath);
32
+ return backupPath;
33
+ }
34
+ }
35
+ function readStoredConfig() {
36
+ try {
37
+ const parsed = JSON.parse(readFileSync(configPath(), "utf8"));
38
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
39
+ throw new SyntaxError("the root value must be an object");
40
+ }
41
+ return parsed;
42
+ } catch (error) {
43
+ if (error?.code === "ENOENT") return {};
44
+ if (error instanceof SyntaxError) {
45
+ throw new Error(
46
+ `Invalid JSON in ${configPath()}: ${error.message}
47
+ Run: npx --yes @impractical-ai/motion setup --claude`
48
+ );
49
+ }
50
+ throw new Error(
51
+ `Could not read ${configPath()}: ${error instanceof Error ? error.message : String(error)}`
52
+ );
53
+ }
54
+ }
55
+ function loadConfig() {
56
+ const stored = readStoredConfig();
57
+ return {
58
+ apiKey: process.env.IMPRACTICAL_API_KEY ?? stored.apiKey,
59
+ supabaseUrl: process.env.IMPRACTICAL_SUPABASE_URL ?? stored.supabaseUrl ?? PRODUCTION_SUPABASE_URL,
60
+ appUrl: process.env.IMPRACTICAL_APP_URL ?? stored.appUrl ?? PRODUCTION_APP_URL
61
+ };
62
+ }
63
+ function saveConfig(patch) {
64
+ const stored = readStoredConfig();
65
+ const next = { ...stored, ...patch };
66
+ mkdirSync(configDir(), { recursive: true });
67
+ writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n");
68
+ chmodSync(configPath(), 384);
69
+ return next;
70
+ }
71
+ function requireConfig() {
72
+ const config = loadConfig();
73
+ if (!config.supabaseUrl) {
74
+ throw new Error("Not configured. Run: npx --yes @impractical-ai/motion setup --claude");
75
+ }
76
+ if (!config.apiKey) {
77
+ throw new Error("Not connected. Run: npx --yes @impractical-ai/motion setup --claude");
78
+ }
79
+ return config;
80
+ }
81
+
82
+ // src/api.ts
83
+ import { AsyncLocalStorage } from "node:async_hooks";
84
+ import { randomUUID } from "node:crypto";
85
+ var admissionContext = new AsyncLocalStorage();
86
+ var ApiError = class extends Error {
87
+ constructor(status, message, fn, details) {
88
+ super(message);
89
+ this.status = status;
90
+ this.fn = fn;
91
+ this.details = details;
92
+ }
93
+ };
94
+ var ApiContractError = class extends Error {
95
+ constructor(fn, expected) {
96
+ super(`${fn} returned an invalid response: expected ${expected}. Please retry; if this persists, update Impractical Motion or contact support.`);
97
+ this.fn = fn;
98
+ }
99
+ };
100
+ function isRecord(value) {
101
+ return typeof value === "object" && value !== null && !Array.isArray(value);
102
+ }
103
+ function bodyRecord(body) {
104
+ return isRecord(body) ? body : {};
105
+ }
106
+ function validateApiResponse(name, body, data) {
107
+ if (!isRecord(data)) throw new ApiContractError(name, "a JSON object");
108
+ const input = bodyRecord(body);
109
+ if (name === "workspace-bootstrap") {
110
+ const billing = data.billing;
111
+ if (typeof data.orgId !== "string" || typeof data.plan !== "string" || typeof data.hasAccess !== "boolean" || !isRecord(billing) || typeof billing.status !== "string") {
112
+ throw new ApiContractError(
113
+ name,
114
+ "an orgId, plan, hasAccess flag, and billing status"
115
+ );
116
+ }
117
+ }
118
+ if (name === "list-projects") {
119
+ const key = input.projectId ? "project" : "projects";
120
+ const value = data[key];
121
+ if (key === "projects" ? !Array.isArray(value) : !isRecord(value)) {
122
+ throw new ApiContractError(name, `an object with a ${key} ${key === "projects" ? "array" : "object"}`);
123
+ }
124
+ }
125
+ if (name === "prepare-generation") {
126
+ if (typeof data.projectId !== "string" || !Array.isArray(data.drafts) || data.drafts.length === 0 || !isRecord(data.drafts[0]) || typeof data.drafts[0].id !== "string") {
127
+ throw new ApiContractError(
128
+ name,
129
+ "a projectId and at least one draft with an id"
130
+ );
131
+ }
132
+ }
133
+ if (name === "get-draft" && (!isRecord(data.files) || typeof data.draftId !== "string")) {
134
+ throw new ApiContractError(name, "draft metadata and a files object");
135
+ }
136
+ if (name === "save-draft" && typeof data.revision !== "number") {
137
+ throw new ApiContractError(name, "a numeric revision");
138
+ }
139
+ if (name === "job-status" && !Array.isArray(data.jobs)) {
140
+ throw new ApiContractError(name, "an object with a jobs array");
141
+ }
142
+ if (name === "start-export" && typeof data.jobId !== "string") {
143
+ throw new ApiContractError(name, "an object with a jobId");
144
+ }
145
+ if (name === "brand-kits") {
146
+ const key = input.action === "list" ? "brands" : input.action === "get" ? "brand" : null;
147
+ if (key && (key === "brands" ? !Array.isArray(data[key]) : !isRecord(data[key]))) {
148
+ throw new ApiContractError(name, `an object with a ${key} ${key === "brands" ? "array" : "object"}`);
149
+ }
150
+ }
151
+ return data;
152
+ }
153
+ async function invoke(name, body = {}) {
154
+ const { apiKey, supabaseUrl } = requireConfig();
155
+ const receipt = admissionContext.getStore();
156
+ return invokeAt(supabaseUrl, name, body, apiKey, receipt);
157
+ }
158
+ async function invokeAt(supabaseUrl, name, body = {}, apiKey, admissionReceipt) {
159
+ const response = await fetch(`${supabaseUrl.replace(/\/$/, "")}/functions/v1/${name}`, {
160
+ method: "POST",
161
+ headers: {
162
+ ...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
163
+ ...admissionReceipt ? { "x-impractical-admission": admissionReceipt } : {},
164
+ "content-type": "application/json"
165
+ },
166
+ body: JSON.stringify(body)
167
+ });
168
+ const text = await response.text();
169
+ let data = null;
170
+ try {
171
+ data = text ? JSON.parse(text) : null;
172
+ } catch {
173
+ }
174
+ if (!response.ok) {
175
+ const details = isRecord(data) ? data : void 0;
176
+ const message = typeof details?.message === "string" && details.message || typeof details?.error === "string" && details.error || text || `${name} failed (${response.status})`;
177
+ throw new ApiError(response.status, message, name, details);
178
+ }
179
+ return validateApiResponse(name, body, data);
180
+ }
181
+ async function admitMcpCall(toolName, callId = randomUUID()) {
182
+ const { apiKey, supabaseUrl } = requireConfig();
183
+ return invokeAt(supabaseUrl, "mcp-admission", { callId, toolName }, apiKey);
184
+ }
185
+ function withAdmissionReceipt(receipt, task) {
186
+ return admissionContext.run(receipt, task);
187
+ }
188
+ async function withMeteredCall(toolName, task) {
189
+ const admission = await admitMcpCall(toolName);
190
+ return withAdmissionReceipt(admission.receipt, task);
191
+ }
192
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
193
+ async function jobStatus(jobIds) {
194
+ const result = await invoke("job-status", { jobIds });
195
+ return result.jobs;
196
+ }
197
+ async function waitForJobs(jobIds, onTick, deadlineMs = 72e5) {
198
+ const deadline = Date.now() + deadlineMs;
199
+ const terminal = /* @__PURE__ */ new Set();
200
+ let latest = [];
201
+ while (terminal.size < jobIds.length) {
202
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for jobs");
203
+ latest = await jobStatus(jobIds);
204
+ for (const row of latest) {
205
+ if (row.status === "completed" || row.status === "failed" || row.status === "canceled") {
206
+ terminal.add(row.id);
207
+ }
208
+ }
209
+ onTick(latest);
210
+ if (terminal.size < jobIds.length) await delay(2e3);
211
+ }
212
+ return latest;
213
+ }
214
+ function studioLink(appUrl, projectId, slug) {
215
+ const base = (appUrl ?? "http://localhost:5173").replace(/\/$/, "");
216
+ return `${base}/projects/${projectId}/studio${slug ? `?scene=${slug}` : ""}`;
217
+ }
218
+
219
+ export {
220
+ recoverCorruptConfig,
221
+ loadConfig,
222
+ saveConfig,
223
+ requireConfig,
224
+ ApiError,
225
+ ApiContractError,
226
+ validateApiResponse,
227
+ invoke,
228
+ invokeAt,
229
+ admitMcpCall,
230
+ withAdmissionReceipt,
231
+ withMeteredCall,
232
+ delay,
233
+ jobStatus,
234
+ waitForJobs,
235
+ studioLink
236
+ };