@impractical-ai/motion 0.1.2 → 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.
- package/README.md +1 -1
- package/dist/api-64X3G4M2.js +30 -0
- package/dist/chunk-EFP4P3WA.js +98 -0
- package/dist/{chunk-G56RK3HD.js → chunk-KC6BJJXH.js} +61 -301
- package/dist/chunk-M77TS5RO.js +236 -0
- package/dist/commands-WYBUEYA6.js +1511 -0
- package/dist/index.js +81 -1551
- package/dist/{stdio-Y5CK7GJJ.js → stdio-JCRFEHDW.js} +364 -312
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ApiContractError,
|
|
4
|
+
ApiError,
|
|
5
|
+
admitMcpCall,
|
|
6
|
+
delay,
|
|
7
|
+
invoke,
|
|
8
|
+
invokeAt,
|
|
9
|
+
jobStatus,
|
|
10
|
+
studioLink,
|
|
11
|
+
validateApiResponse,
|
|
12
|
+
waitForJobs,
|
|
13
|
+
withAdmissionReceipt,
|
|
14
|
+
withMeteredCall
|
|
15
|
+
} from "./chunk-M77TS5RO.js";
|
|
16
|
+
import "./chunk-EFP4P3WA.js";
|
|
17
|
+
export {
|
|
18
|
+
ApiContractError,
|
|
19
|
+
ApiError,
|
|
20
|
+
admitMcpCall,
|
|
21
|
+
delay,
|
|
22
|
+
invoke,
|
|
23
|
+
invokeAt,
|
|
24
|
+
jobStatus,
|
|
25
|
+
studioLink,
|
|
26
|
+
validateApiResponse,
|
|
27
|
+
waitForJobs,
|
|
28
|
+
withAdmissionReceipt,
|
|
29
|
+
withMeteredCall
|
|
30
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
// package.json
|
|
33
|
+
var package_default = {
|
|
34
|
+
name: "@impractical-ai/motion",
|
|
35
|
+
version: "0.1.4",
|
|
36
|
+
private: false,
|
|
37
|
+
description: "Create and refine Impractical Motion videos from coding agents and the terminal.",
|
|
38
|
+
homepage: "https://github.com/Openpod/impractical-motion#readme",
|
|
39
|
+
repository: {
|
|
40
|
+
type: "git",
|
|
41
|
+
url: "git+https://github.com/Openpod/impractical-motion.git"
|
|
42
|
+
},
|
|
43
|
+
bugs: {
|
|
44
|
+
url: "https://github.com/Openpod/impractical-motion/issues"
|
|
45
|
+
},
|
|
46
|
+
license: "UNLICENSED",
|
|
47
|
+
type: "module",
|
|
48
|
+
files: [
|
|
49
|
+
"dist",
|
|
50
|
+
"assets",
|
|
51
|
+
"README.md"
|
|
52
|
+
],
|
|
53
|
+
bin: {
|
|
54
|
+
impractical: "dist/index.js"
|
|
55
|
+
},
|
|
56
|
+
engines: {
|
|
57
|
+
node: ">=22"
|
|
58
|
+
},
|
|
59
|
+
scripts: {
|
|
60
|
+
build: 'node ../scripts/build-engine-dts.mjs && rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --splitting --outdir=dist --external:playwright-core --banner:js="#!/usr/bin/env node" && chmod +x dist/index.js',
|
|
61
|
+
typecheck: "tsc --noEmit",
|
|
62
|
+
test: "npm run build && esbuild tests/onboarding.test.ts tests/mcp-tools.test.ts tests/mcp-frames.test.ts tests/engine-dts.test.ts tests/mcp-connect.test.ts tests/mcp-longpoll.test.ts --bundle --platform=node --format=esm --packages=external --outdir=dist --out-extension:.js=.mjs && node --test dist/onboarding.test.mjs dist/mcp-tools.test.mjs dist/mcp-frames.test.mjs dist/engine-dts.test.mjs dist/mcp-connect.test.mjs dist/mcp-longpoll.test.mjs",
|
|
63
|
+
prepack: "npm run build"
|
|
64
|
+
},
|
|
65
|
+
publishConfig: {
|
|
66
|
+
access: "public"
|
|
67
|
+
},
|
|
68
|
+
devDependencies: {
|
|
69
|
+
esbuild: "^0.25.0",
|
|
70
|
+
typescript: "^5.6.3"
|
|
71
|
+
},
|
|
72
|
+
dependencies: {
|
|
73
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
74
|
+
"playwright-core": "^1.55.1",
|
|
75
|
+
zod: "^4.4.3"
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/constants.ts
|
|
80
|
+
if (package_default.name !== "@impractical-ai/motion" || typeof package_default.version !== "string" || !package_default.version) {
|
|
81
|
+
throw new Error("The packaged CLI manifest is missing valid release metadata");
|
|
82
|
+
}
|
|
83
|
+
var PRODUCTION_APP_URL = "https://motion.impractical.ai";
|
|
84
|
+
var PRODUCTION_SUPABASE_URL = "https://uaxjweednprtbhbceify.supabase.co";
|
|
85
|
+
var PACKAGE_NAME = "@impractical-ai/motion";
|
|
86
|
+
var PACKAGE_VERSION = package_default.version;
|
|
87
|
+
var PACKAGE_COMMAND = `npx --yes ${PACKAGE_NAME}`;
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
__commonJS,
|
|
91
|
+
__export,
|
|
92
|
+
__toESM,
|
|
93
|
+
PRODUCTION_APP_URL,
|
|
94
|
+
PRODUCTION_SUPABASE_URL,
|
|
95
|
+
PACKAGE_NAME,
|
|
96
|
+
PACKAGE_VERSION,
|
|
97
|
+
PACKAGE_COMMAND
|
|
98
|
+
};
|
|
@@ -1,260 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
for (let key of __getOwnPropNames(from))
|
|
18
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
-
}
|
|
21
|
-
return to;
|
|
22
|
-
};
|
|
23
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
-
mod
|
|
30
|
-
));
|
|
31
|
-
|
|
32
|
-
// src/constants.ts
|
|
33
|
-
var PRODUCTION_APP_URL = "https://motion.impractical.ai";
|
|
34
|
-
var PRODUCTION_SUPABASE_URL = "https://uaxjweednprtbhbceify.supabase.co";
|
|
35
|
-
var PACKAGE_NAME = "@impractical-ai/motion";
|
|
36
|
-
var PACKAGE_VERSION = "0.1.2";
|
|
37
|
-
var PACKAGE_COMMAND = `npx --yes ${PACKAGE_NAME}`;
|
|
38
|
-
|
|
39
|
-
// src/config.ts
|
|
40
|
-
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync, renameSync } from "node:fs";
|
|
41
|
-
import { homedir } from "node:os";
|
|
42
|
-
import { join } from "node:path";
|
|
43
|
-
var configDir = () => join(homedir(), ".impractical");
|
|
44
|
-
var configPath = () => join(configDir(), "config.json");
|
|
45
|
-
function recoverCorruptConfig() {
|
|
46
|
-
const path = configPath();
|
|
47
|
-
if (!existsSync(path)) return null;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
50
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
51
|
-
throw new SyntaxError("the root value must be an object");
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (!(error instanceof SyntaxError)) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
`Could not inspect ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
let backupPath = `${path}.corrupt-${Date.now()}`;
|
|
61
|
-
let suffix = 1;
|
|
62
|
-
while (existsSync(backupPath)) backupPath = `${path}.corrupt-${Date.now()}-${suffix++}`;
|
|
63
|
-
renameSync(path, backupPath);
|
|
64
|
-
return backupPath;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function readStoredConfig() {
|
|
68
|
-
try {
|
|
69
|
-
const parsed = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
70
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
71
|
-
throw new SyntaxError("the root value must be an object");
|
|
72
|
-
}
|
|
73
|
-
return parsed;
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if (error?.code === "ENOENT") return {};
|
|
76
|
-
if (error instanceof SyntaxError) {
|
|
77
|
-
throw new Error(`Invalid JSON in ${configPath()}: ${error.message}`);
|
|
78
|
-
}
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Could not read ${configPath()}: ${error instanceof Error ? error.message : String(error)}`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function loadConfig() {
|
|
85
|
-
const stored = readStoredConfig();
|
|
86
|
-
return {
|
|
87
|
-
apiKey: process.env.IMPRACTICAL_API_KEY ?? stored.apiKey,
|
|
88
|
-
supabaseUrl: process.env.IMPRACTICAL_SUPABASE_URL ?? stored.supabaseUrl ?? PRODUCTION_SUPABASE_URL,
|
|
89
|
-
appUrl: process.env.IMPRACTICAL_APP_URL ?? stored.appUrl ?? PRODUCTION_APP_URL
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function saveConfig(patch) {
|
|
93
|
-
const stored = readStoredConfig();
|
|
94
|
-
const next = { ...stored, ...patch };
|
|
95
|
-
mkdirSync(configDir(), { recursive: true });
|
|
96
|
-
writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n");
|
|
97
|
-
chmodSync(configPath(), 384);
|
|
98
|
-
return next;
|
|
99
|
-
}
|
|
100
|
-
function requireConfig() {
|
|
101
|
-
const config = loadConfig();
|
|
102
|
-
if (!config.supabaseUrl) {
|
|
103
|
-
throw new Error("Not configured. Run: npx --yes @impractical-ai/motion setup --claude");
|
|
104
|
-
}
|
|
105
|
-
if (!config.apiKey) {
|
|
106
|
-
throw new Error("Not connected. Run: npx --yes @impractical-ai/motion setup --claude");
|
|
107
|
-
}
|
|
108
|
-
return config;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// src/api.ts
|
|
112
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
113
|
-
import { randomUUID } from "node:crypto";
|
|
114
|
-
var admissionContext = new AsyncLocalStorage();
|
|
115
|
-
var ApiError = class extends Error {
|
|
116
|
-
constructor(status, message, fn, details) {
|
|
117
|
-
super(message);
|
|
118
|
-
this.status = status;
|
|
119
|
-
this.fn = fn;
|
|
120
|
-
this.details = details;
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
var ApiContractError = class extends Error {
|
|
124
|
-
constructor(fn, expected) {
|
|
125
|
-
super(`${fn} returned an invalid response: expected ${expected}. Please retry; if this persists, update Impractical Motion or contact support.`);
|
|
126
|
-
this.fn = fn;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
function isRecord(value) {
|
|
130
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
131
|
-
}
|
|
132
|
-
function bodyRecord(body) {
|
|
133
|
-
return isRecord(body) ? body : {};
|
|
134
|
-
}
|
|
135
|
-
function validateApiResponse(name, body, data) {
|
|
136
|
-
if (!isRecord(data)) throw new ApiContractError(name, "a JSON object");
|
|
137
|
-
const input = bodyRecord(body);
|
|
138
|
-
if (name === "workspace-bootstrap") {
|
|
139
|
-
const billing = data.billing;
|
|
140
|
-
if (typeof data.orgId !== "string" || typeof data.plan !== "string" || typeof data.hasAccess !== "boolean" || !isRecord(billing) || typeof billing.status !== "string") {
|
|
141
|
-
throw new ApiContractError(
|
|
142
|
-
name,
|
|
143
|
-
"an orgId, plan, hasAccess flag, and billing status"
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (name === "list-projects") {
|
|
148
|
-
const key = input.projectId ? "project" : "projects";
|
|
149
|
-
const value = data[key];
|
|
150
|
-
if (key === "projects" ? !Array.isArray(value) : !isRecord(value)) {
|
|
151
|
-
throw new ApiContractError(name, `an object with a ${key} ${key === "projects" ? "array" : "object"}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (name === "prepare-generation") {
|
|
155
|
-
if (typeof data.projectId !== "string" || !Array.isArray(data.drafts) || data.drafts.length === 0 || !isRecord(data.drafts[0]) || typeof data.drafts[0].id !== "string") {
|
|
156
|
-
throw new ApiContractError(
|
|
157
|
-
name,
|
|
158
|
-
"a projectId and at least one draft with an id"
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (name === "get-draft" && (!isRecord(data.files) || typeof data.draftId !== "string")) {
|
|
163
|
-
throw new ApiContractError(name, "draft metadata and a files object");
|
|
164
|
-
}
|
|
165
|
-
if (name === "save-draft" && typeof data.revision !== "number") {
|
|
166
|
-
throw new ApiContractError(name, "a numeric revision");
|
|
167
|
-
}
|
|
168
|
-
if (name === "job-status" && !Array.isArray(data.jobs)) {
|
|
169
|
-
throw new ApiContractError(name, "an object with a jobs array");
|
|
170
|
-
}
|
|
171
|
-
if (name === "start-export" && typeof data.jobId !== "string") {
|
|
172
|
-
throw new ApiContractError(name, "an object with a jobId");
|
|
173
|
-
}
|
|
174
|
-
if (name === "brand-kits") {
|
|
175
|
-
const key = input.action === "list" ? "brands" : input.action === "get" ? "brand" : null;
|
|
176
|
-
if (key && (key === "brands" ? !Array.isArray(data[key]) : !isRecord(data[key]))) {
|
|
177
|
-
throw new ApiContractError(name, `an object with a ${key} ${key === "brands" ? "array" : "object"}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return data;
|
|
181
|
-
}
|
|
182
|
-
async function invoke(name, body = {}) {
|
|
183
|
-
const { apiKey, supabaseUrl } = requireConfig();
|
|
184
|
-
const receipt = admissionContext.getStore();
|
|
185
|
-
return invokeAt(supabaseUrl, name, body, apiKey, receipt);
|
|
186
|
-
}
|
|
187
|
-
async function invokeAt(supabaseUrl, name, body = {}, apiKey, admissionReceipt) {
|
|
188
|
-
const response = await fetch(`${supabaseUrl.replace(/\/$/, "")}/functions/v1/${name}`, {
|
|
189
|
-
method: "POST",
|
|
190
|
-
headers: {
|
|
191
|
-
...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
|
|
192
|
-
...admissionReceipt ? { "x-impractical-admission": admissionReceipt } : {},
|
|
193
|
-
"content-type": "application/json"
|
|
194
|
-
},
|
|
195
|
-
body: JSON.stringify(body)
|
|
196
|
-
});
|
|
197
|
-
const text = await response.text();
|
|
198
|
-
let data = null;
|
|
199
|
-
try {
|
|
200
|
-
data = text ? JSON.parse(text) : null;
|
|
201
|
-
} catch {
|
|
202
|
-
}
|
|
203
|
-
if (!response.ok) {
|
|
204
|
-
const details = isRecord(data) ? data : void 0;
|
|
205
|
-
const message = typeof details?.message === "string" && details.message || typeof details?.error === "string" && details.error || text || `${name} failed (${response.status})`;
|
|
206
|
-
throw new ApiError(response.status, message, name, details);
|
|
207
|
-
}
|
|
208
|
-
return validateApiResponse(name, body, data);
|
|
209
|
-
}
|
|
210
|
-
async function admitMcpCall(toolName, callId = randomUUID()) {
|
|
211
|
-
const { apiKey, supabaseUrl } = requireConfig();
|
|
212
|
-
return invokeAt(supabaseUrl, "mcp-admission", { callId, toolName }, apiKey);
|
|
213
|
-
}
|
|
214
|
-
function withAdmissionReceipt(receipt, task) {
|
|
215
|
-
return admissionContext.run(receipt, task);
|
|
216
|
-
}
|
|
217
|
-
async function withMeteredCall(toolName, task) {
|
|
218
|
-
const admission = await admitMcpCall(toolName);
|
|
219
|
-
return withAdmissionReceipt(admission.receipt, task);
|
|
220
|
-
}
|
|
221
|
-
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
222
|
-
async function jobStatus(jobIds) {
|
|
223
|
-
const result = await invoke("job-status", { jobIds });
|
|
224
|
-
return result.jobs;
|
|
225
|
-
}
|
|
226
|
-
async function waitForJobs(jobIds, onTick, deadlineMs = 72e5) {
|
|
227
|
-
const deadline = Date.now() + deadlineMs;
|
|
228
|
-
const terminal = /* @__PURE__ */ new Set();
|
|
229
|
-
let latest = [];
|
|
230
|
-
while (terminal.size < jobIds.length) {
|
|
231
|
-
if (Date.now() >= deadline) throw new Error("Timed out waiting for jobs");
|
|
232
|
-
latest = await jobStatus(jobIds);
|
|
233
|
-
for (const row of latest) {
|
|
234
|
-
if (row.status === "completed" || row.status === "failed" || row.status === "canceled") {
|
|
235
|
-
terminal.add(row.id);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
onTick(latest);
|
|
239
|
-
if (terminal.size < jobIds.length) await delay(2e3);
|
|
240
|
-
}
|
|
241
|
-
return latest;
|
|
242
|
-
}
|
|
243
|
-
function studioLink(appUrl, projectId, slug) {
|
|
244
|
-
const base = (appUrl ?? "http://localhost:5173").replace(/\/$/, "");
|
|
245
|
-
return `${base}/projects/${projectId}/studio${slug ? `?scene=${slug}` : ""}`;
|
|
246
|
-
}
|
|
2
|
+
import {
|
|
3
|
+
ApiError,
|
|
4
|
+
delay,
|
|
5
|
+
invokeAt,
|
|
6
|
+
loadConfig,
|
|
7
|
+
recoverCorruptConfig,
|
|
8
|
+
saveConfig
|
|
9
|
+
} from "./chunk-M77TS5RO.js";
|
|
10
|
+
import {
|
|
11
|
+
PACKAGE_COMMAND,
|
|
12
|
+
PACKAGE_NAME,
|
|
13
|
+
PACKAGE_VERSION,
|
|
14
|
+
PRODUCTION_APP_URL,
|
|
15
|
+
PRODUCTION_SUPABASE_URL
|
|
16
|
+
} from "./chunk-EFP4P3WA.js";
|
|
247
17
|
|
|
248
18
|
// src/onboarding.ts
|
|
249
|
-
import { existsSync
|
|
250
|
-
import { homedir
|
|
251
|
-
import { dirname, join
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
252
22
|
import { fileURLToPath } from "node:url";
|
|
253
23
|
import { spawnSync } from "node:child_process";
|
|
254
24
|
var CLAUDE_MARKETPLACE = "Openpod/impractical-agent-plugins";
|
|
255
25
|
var CLAUDE_PLUGIN = "impractical-motion@impractical";
|
|
256
26
|
var CLAUDE_MARKETPLACE_NAME = "impractical";
|
|
257
27
|
var AUTH_VERIFY_MIN_VERSION = "0.1.2";
|
|
28
|
+
var CLAUDE_INSTALL_COMMAND = "npm install -g @anthropic-ai/claude-code";
|
|
29
|
+
var CLAUDE_INSTALL_URL = "https://code.claude.com/docs/en/installation";
|
|
258
30
|
function parseSemver(value) {
|
|
259
31
|
const match = value.trim().match(
|
|
260
32
|
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
@@ -294,20 +66,20 @@ function compareSemver(left, right) {
|
|
|
294
66
|
return 0;
|
|
295
67
|
}
|
|
296
68
|
function readJson(path) {
|
|
297
|
-
if (!
|
|
69
|
+
if (!existsSync(path)) return void 0;
|
|
298
70
|
try {
|
|
299
|
-
return JSON.parse(
|
|
71
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
300
72
|
} catch {
|
|
301
73
|
return void 0;
|
|
302
74
|
}
|
|
303
75
|
}
|
|
304
|
-
var claudeConfigDir = () => process.env.CLAUDE_CONFIG_DIR?.trim() ||
|
|
305
|
-
function marketplaceRecord(
|
|
306
|
-
const value = readJson(
|
|
76
|
+
var claudeConfigDir = () => process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
|
|
77
|
+
function marketplaceRecord(configDir) {
|
|
78
|
+
const value = readJson(join(configDir, "plugins", "known_marketplaces.json"));
|
|
307
79
|
return value?.impractical;
|
|
308
80
|
}
|
|
309
|
-
function installedPluginVersion(
|
|
310
|
-
const value = readJson(
|
|
81
|
+
function installedPluginVersion(configDir) {
|
|
82
|
+
const value = readJson(join(configDir, "plugins", "installed_plugins.json"));
|
|
311
83
|
const entries = value?.plugins?.[CLAUDE_PLUGIN];
|
|
312
84
|
if (!Array.isArray(entries)) return void 0;
|
|
313
85
|
const versions = entries.filter((entry) => entry?.scope === "user" && typeof entry.version === "string").map((entry) => entry.version);
|
|
@@ -317,10 +89,10 @@ function installedPluginVersion(configDir2) {
|
|
|
317
89
|
return comparison !== void 0 && comparison > 0 ? version : highest;
|
|
318
90
|
}, void 0);
|
|
319
91
|
}
|
|
320
|
-
function marketplacePluginVersion(
|
|
321
|
-
const record = marketplaceRecord(
|
|
322
|
-
const location = typeof record?.installLocation === "string" ? record.installLocation :
|
|
323
|
-
const value = readJson(
|
|
92
|
+
function marketplacePluginVersion(configDir) {
|
|
93
|
+
const record = marketplaceRecord(configDir);
|
|
94
|
+
const location = typeof record?.installLocation === "string" ? record.installLocation : join(configDir, "plugins", "marketplaces", CLAUDE_MARKETPLACE_NAME);
|
|
95
|
+
const value = readJson(join(location, ".claude-plugin", "marketplace.json"));
|
|
324
96
|
return value?.plugins?.find((plugin) => plugin.name === "impractical-motion")?.version;
|
|
325
97
|
}
|
|
326
98
|
function expectedMarketplace(record) {
|
|
@@ -337,6 +109,13 @@ function commandResult(command, args) {
|
|
|
337
109
|
function requireCommand(command, label) {
|
|
338
110
|
const result = commandResult(command, ["--version"]);
|
|
339
111
|
if (result.error || result.status !== 0) {
|
|
112
|
+
if (command === "claude") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`${label} is not installed. Install it, then rerun setup:
|
|
115
|
+
${CLAUDE_INSTALL_COMMAND}
|
|
116
|
+
Official setup guide: ${CLAUDE_INSTALL_URL}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
340
119
|
throw new Error(`${label} is required before setup can continue.`);
|
|
341
120
|
}
|
|
342
121
|
}
|
|
@@ -345,7 +124,7 @@ function hasChrome() {
|
|
|
345
124
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
346
125
|
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
347
126
|
];
|
|
348
|
-
if (macPaths.some(
|
|
127
|
+
if (macPaths.some(existsSync)) return true;
|
|
349
128
|
return ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"].some((command) => {
|
|
350
129
|
const result = spawnSync(command, ["--version"], { stdio: "ignore", windowsHide: true });
|
|
351
130
|
return !result.error && result.status === 0;
|
|
@@ -381,9 +160,9 @@ ${result.stderr ?? ""}`.trim();
|
|
|
381
160
|
}
|
|
382
161
|
function installClaudePlugin(log = () => {
|
|
383
162
|
}) {
|
|
384
|
-
const
|
|
385
|
-
const beforeVersion = installedPluginVersion(
|
|
386
|
-
let record = marketplaceRecord(
|
|
163
|
+
const configDir = claudeConfigDir();
|
|
164
|
+
const beforeVersion = installedPluginVersion(configDir);
|
|
165
|
+
let record = marketplaceRecord(configDir);
|
|
387
166
|
const preserveSource = Boolean(record && !expectedMarketplace(record));
|
|
388
167
|
if (preserveSource) {
|
|
389
168
|
log(
|
|
@@ -391,12 +170,12 @@ function installClaudePlugin(log = () => {
|
|
|
391
170
|
);
|
|
392
171
|
} else if (!record) {
|
|
393
172
|
runClaude(["plugin", "marketplace", "add", CLAUDE_MARKETPLACE], log, true);
|
|
394
|
-
record = marketplaceRecord(
|
|
173
|
+
record = marketplaceRecord(configDir);
|
|
395
174
|
}
|
|
396
175
|
if (!beforeVersion) {
|
|
397
176
|
runClaude(["plugin", "install", CLAUDE_PLUGIN, "--scope", "user"], log, true);
|
|
398
177
|
} else if (!preserveSource) {
|
|
399
|
-
const advertisedVersion = marketplacePluginVersion(
|
|
178
|
+
const advertisedVersion = marketplacePluginVersion(configDir);
|
|
400
179
|
const comparison = advertisedVersion ? compareSemver(beforeVersion, advertisedVersion) : void 0;
|
|
401
180
|
if (comparison === void 0) {
|
|
402
181
|
log(
|
|
@@ -408,8 +187,8 @@ function installClaudePlugin(log = () => {
|
|
|
408
187
|
);
|
|
409
188
|
} else {
|
|
410
189
|
runClaude(["plugin", "marketplace", "update", CLAUDE_MARKETPLACE_NAME], log, true);
|
|
411
|
-
const refreshedVersion = marketplacePluginVersion(
|
|
412
|
-
const currentVersion = installedPluginVersion(
|
|
190
|
+
const refreshedVersion = marketplacePluginVersion(configDir);
|
|
191
|
+
const currentVersion = installedPluginVersion(configDir) ?? beforeVersion;
|
|
413
192
|
const refreshedComparison = refreshedVersion ? compareSemver(currentVersion, refreshedVersion) : void 0;
|
|
414
193
|
if (refreshedComparison !== void 0 && refreshedComparison < 0) {
|
|
415
194
|
runClaude(["plugin", "update", CLAUDE_PLUGIN], log, true);
|
|
@@ -420,7 +199,7 @@ function installClaudePlugin(log = () => {
|
|
|
420
199
|
}
|
|
421
200
|
}
|
|
422
201
|
}
|
|
423
|
-
const afterVersion = installedPluginVersion(
|
|
202
|
+
const afterVersion = installedPluginVersion(configDir);
|
|
424
203
|
const change = beforeVersion && afterVersion ? compareSemver(afterVersion, beforeVersion) : void 0;
|
|
425
204
|
if (change !== void 0 && change < 0) {
|
|
426
205
|
log(`WARNING: ${CLAUDE_PLUGIN} resolved to ${afterVersion}, below its prior ${beforeVersion}.`);
|
|
@@ -454,44 +233,44 @@ function pluginResultLine(result) {
|
|
|
454
233
|
return `${CLAUDE_PLUGIN} already at ${result.afterVersion}; left unchanged.`;
|
|
455
234
|
}
|
|
456
235
|
var skillSourcePath = () => fileURLToPath(new URL("../assets/impractical/SKILL.md", import.meta.url));
|
|
457
|
-
function installSkills(home =
|
|
458
|
-
const source =
|
|
236
|
+
function installSkills(home = homedir(), sourcePath = skillSourcePath()) {
|
|
237
|
+
const source = readFileSync(sourcePath, "utf8");
|
|
459
238
|
const targets = [
|
|
460
239
|
{
|
|
461
240
|
harness: "Claude Code",
|
|
462
|
-
path:
|
|
241
|
+
path: join(home, ".claude", "skills", "impractical", "SKILL.md")
|
|
463
242
|
},
|
|
464
243
|
{
|
|
465
244
|
harness: "Codex",
|
|
466
|
-
path:
|
|
245
|
+
path: join(home, ".codex", "skills", "impractical", "SKILL.md")
|
|
467
246
|
}
|
|
468
247
|
];
|
|
469
248
|
for (const target of targets) {
|
|
470
|
-
|
|
471
|
-
|
|
249
|
+
mkdirSync(dirname(target.path), { recursive: true, mode: 448 });
|
|
250
|
+
writeFileSync(target.path, source, { mode: 420 });
|
|
472
251
|
}
|
|
473
252
|
return targets;
|
|
474
253
|
}
|
|
475
|
-
function reconcileLegacySkills(home =
|
|
476
|
-
const source =
|
|
254
|
+
function reconcileLegacySkills(home = homedir(), sourcePath = skillSourcePath()) {
|
|
255
|
+
const source = readFileSync(sourcePath, "utf8");
|
|
477
256
|
const targets = [
|
|
478
257
|
{
|
|
479
258
|
harness: "Claude Code",
|
|
480
|
-
path:
|
|
259
|
+
path: join(home, ".claude", "skills", "impractical", "SKILL.md")
|
|
481
260
|
},
|
|
482
261
|
{
|
|
483
262
|
harness: "Codex",
|
|
484
|
-
path:
|
|
263
|
+
path: join(home, ".codex", "skills", "impractical", "SKILL.md")
|
|
485
264
|
}
|
|
486
265
|
];
|
|
487
266
|
const reconciled = [];
|
|
488
267
|
for (const target of targets) {
|
|
489
|
-
if (!
|
|
490
|
-
const previous =
|
|
268
|
+
if (!existsSync(target.path)) continue;
|
|
269
|
+
const previous = readFileSync(target.path, "utf8");
|
|
491
270
|
if (previous === source) continue;
|
|
492
271
|
const backupPath = `${target.path}.pre-plugin`;
|
|
493
|
-
if (!
|
|
494
|
-
|
|
272
|
+
if (!existsSync(backupPath)) writeFileSync(backupPath, previous, { mode: 420 });
|
|
273
|
+
writeFileSync(target.path, source, { mode: 420 });
|
|
495
274
|
reconciled.push({ ...target, backupPath });
|
|
496
275
|
}
|
|
497
276
|
return reconciled;
|
|
@@ -593,25 +372,6 @@ async function setup(options) {
|
|
|
593
372
|
}
|
|
594
373
|
|
|
595
374
|
export {
|
|
596
|
-
__commonJS,
|
|
597
|
-
__export,
|
|
598
|
-
__toESM,
|
|
599
|
-
PRODUCTION_APP_URL,
|
|
600
|
-
PRODUCTION_SUPABASE_URL,
|
|
601
|
-
PACKAGE_VERSION,
|
|
602
|
-
loadConfig,
|
|
603
|
-
saveConfig,
|
|
604
|
-
requireConfig,
|
|
605
|
-
ApiError,
|
|
606
|
-
validateApiResponse,
|
|
607
|
-
invoke,
|
|
608
|
-
invokeAt,
|
|
609
|
-
admitMcpCall,
|
|
610
|
-
withAdmissionReceipt,
|
|
611
|
-
withMeteredCall,
|
|
612
|
-
jobStatus,
|
|
613
|
-
waitForJobs,
|
|
614
|
-
studioLink,
|
|
615
375
|
openBrowser,
|
|
616
376
|
setup
|
|
617
377
|
};
|