@impractical-ai/motion 0.1.0 → 0.1.2

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,617 @@
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
+ // 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
+ }
247
+
248
+ // src/onboarding.ts
249
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
250
+ import { homedir as homedir2 } from "node:os";
251
+ import { dirname, join as join2 } from "node:path";
252
+ import { fileURLToPath } from "node:url";
253
+ import { spawnSync } from "node:child_process";
254
+ var CLAUDE_MARKETPLACE = "Openpod/impractical-agent-plugins";
255
+ var CLAUDE_PLUGIN = "impractical-motion@impractical";
256
+ var CLAUDE_MARKETPLACE_NAME = "impractical";
257
+ var AUTH_VERIFY_MIN_VERSION = "0.1.2";
258
+ function parseSemver(value) {
259
+ const match = value.trim().match(
260
+ /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
261
+ );
262
+ if (!match) return void 0;
263
+ const prerelease = match[4] ? match[4].split(".").map((part) => /^(?:0|[1-9]\d*)$/.test(part) ? Number(part) : part) : [];
264
+ return {
265
+ main: [Number(match[1]), Number(match[2]), Number(match[3])],
266
+ prerelease
267
+ };
268
+ }
269
+ function compareSemver(left, right) {
270
+ const a = parseSemver(left);
271
+ const b = parseSemver(right);
272
+ if (!a || !b) return void 0;
273
+ for (let index = 0; index < a.main.length; index++) {
274
+ if (a.main[index] !== b.main[index]) return a.main[index] < b.main[index] ? -1 : 1;
275
+ }
276
+ if (!a.prerelease.length || !b.prerelease.length) {
277
+ if (a.prerelease.length === b.prerelease.length) return 0;
278
+ return a.prerelease.length ? -1 : 1;
279
+ }
280
+ for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index++) {
281
+ const leftPart = a.prerelease[index];
282
+ const rightPart = b.prerelease[index];
283
+ if (leftPart === void 0 || rightPart === void 0) {
284
+ return leftPart === rightPart ? 0 : leftPart === void 0 ? -1 : 1;
285
+ }
286
+ if (leftPart === rightPart) continue;
287
+ if (typeof leftPart === "number" && typeof rightPart === "number") {
288
+ return leftPart < rightPart ? -1 : 1;
289
+ }
290
+ if (typeof leftPart === "number") return -1;
291
+ if (typeof rightPart === "number") return 1;
292
+ return leftPart < rightPart ? -1 : 1;
293
+ }
294
+ return 0;
295
+ }
296
+ function readJson(path) {
297
+ if (!existsSync2(path)) return void 0;
298
+ try {
299
+ return JSON.parse(readFileSync2(path, "utf8"));
300
+ } catch {
301
+ return void 0;
302
+ }
303
+ }
304
+ var claudeConfigDir = () => process.env.CLAUDE_CONFIG_DIR?.trim() || join2(homedir2(), ".claude");
305
+ function marketplaceRecord(configDir2) {
306
+ const value = readJson(join2(configDir2, "plugins", "known_marketplaces.json"));
307
+ return value?.impractical;
308
+ }
309
+ function installedPluginVersion(configDir2) {
310
+ const value = readJson(join2(configDir2, "plugins", "installed_plugins.json"));
311
+ const entries = value?.plugins?.[CLAUDE_PLUGIN];
312
+ if (!Array.isArray(entries)) return void 0;
313
+ const versions = entries.filter((entry) => entry?.scope === "user" && typeof entry.version === "string").map((entry) => entry.version);
314
+ return versions.reduce((highest, version) => {
315
+ if (!highest) return version;
316
+ const comparison = compareSemver(version, highest);
317
+ return comparison !== void 0 && comparison > 0 ? version : highest;
318
+ }, void 0);
319
+ }
320
+ function marketplacePluginVersion(configDir2) {
321
+ const record = marketplaceRecord(configDir2);
322
+ const location = typeof record?.installLocation === "string" ? record.installLocation : join2(configDir2, "plugins", "marketplaces", CLAUDE_MARKETPLACE_NAME);
323
+ const value = readJson(join2(location, ".claude-plugin", "marketplace.json"));
324
+ return value?.plugins?.find((plugin) => plugin.name === "impractical-motion")?.version;
325
+ }
326
+ function expectedMarketplace(record) {
327
+ const source = record?.source;
328
+ return source?.source === "github" && source.repo?.toLowerCase() === CLAUDE_MARKETPLACE.toLowerCase() && Object.keys(source).every((key) => key === "source" || key === "repo");
329
+ }
330
+ function marketplaceSourceLabel(record) {
331
+ if (record.source?.source === "directory") return "a local directory source";
332
+ return `a ${record.source?.source ?? "custom"} source`;
333
+ }
334
+ function commandResult(command, args) {
335
+ return spawnSync(command, args, { encoding: "utf8", windowsHide: true });
336
+ }
337
+ function requireCommand(command, label) {
338
+ const result = commandResult(command, ["--version"]);
339
+ if (result.error || result.status !== 0) {
340
+ throw new Error(`${label} is required before setup can continue.`);
341
+ }
342
+ }
343
+ function hasChrome() {
344
+ const macPaths = [
345
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
346
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
347
+ ];
348
+ if (macPaths.some(existsSync2)) return true;
349
+ return ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"].some((command) => {
350
+ const result = spawnSync(command, ["--version"], { stdio: "ignore", windowsHide: true });
351
+ return !result.error && result.status === 0;
352
+ });
353
+ }
354
+ function verifyClaudePrerequisites() {
355
+ const major = Number(process.versions.node.split(".")[0]);
356
+ if (!Number.isInteger(major) || major < 22) throw new Error("Node.js 22 or newer is required.");
357
+ requireCommand("claude", "Claude Code");
358
+ if (!hasChrome()) throw new Error("Google Chrome or Chromium is required to inspect video frames.");
359
+ }
360
+ function runClaude(args, log, tolerateAlreadyInstalled = false) {
361
+ const result = commandResult("claude", args);
362
+ const detail = `${result.stdout ?? ""}
363
+ ${result.stderr ?? ""}`.trim();
364
+ if (!result.error && result.status === 0) {
365
+ if (detail) log(detail);
366
+ const versionChange = detail.match(
367
+ /updated from\s+v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\s+to\s+v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/i
368
+ );
369
+ if (versionChange && (compareSemver(versionChange[2], versionChange[1]) ?? 0) < 0) {
370
+ log(
371
+ `WARNING: Claude reported a plugin downgrade ${versionChange[1]} \u2192 ${versionChange[2]}. The installed plugin may no longer include the timeout fix.`
372
+ );
373
+ }
374
+ return detail;
375
+ }
376
+ if (tolerateAlreadyInstalled && /already (?:added|exists|installed|configured)/i.test(detail)) {
377
+ if (detail) log(detail);
378
+ return detail;
379
+ }
380
+ throw new Error(`Claude plugin setup failed: ${detail || result.error?.message || "unknown error"}`);
381
+ }
382
+ function installClaudePlugin(log = () => {
383
+ }) {
384
+ const configDir2 = claudeConfigDir();
385
+ const beforeVersion = installedPluginVersion(configDir2);
386
+ let record = marketplaceRecord(configDir2);
387
+ const preserveSource = Boolean(record && !expectedMarketplace(record));
388
+ if (preserveSource) {
389
+ log(
390
+ `Marketplace "${CLAUDE_MARKETPLACE_NAME}" already points at ${marketplaceSourceLabel(record)} \u2014 leaving it untouched. Run \`claude plugin marketplace add ${CLAUDE_MARKETPLACE}\` yourself to switch sources.`
391
+ );
392
+ } else if (!record) {
393
+ runClaude(["plugin", "marketplace", "add", CLAUDE_MARKETPLACE], log, true);
394
+ record = marketplaceRecord(configDir2);
395
+ }
396
+ if (!beforeVersion) {
397
+ runClaude(["plugin", "install", CLAUDE_PLUGIN, "--scope", "user"], log, true);
398
+ } else if (!preserveSource) {
399
+ const advertisedVersion = marketplacePluginVersion(configDir2);
400
+ const comparison = advertisedVersion ? compareSemver(beforeVersion, advertisedVersion) : void 0;
401
+ if (comparison === void 0) {
402
+ log(
403
+ `${CLAUDE_PLUGIN} ${beforeVersion} is installed, but the marketplace version could not be compared safely \u2014 left unchanged.`
404
+ );
405
+ } else if (comparison >= 0) {
406
+ log(
407
+ `${CLAUDE_PLUGIN} ${beforeVersion} already installed` + (comparison > 0 ? ` (newer than marketplace ${advertisedVersion})` : "") + " \u2014 left unchanged."
408
+ );
409
+ } else {
410
+ runClaude(["plugin", "marketplace", "update", CLAUDE_MARKETPLACE_NAME], log, true);
411
+ const refreshedVersion = marketplacePluginVersion(configDir2);
412
+ const currentVersion = installedPluginVersion(configDir2) ?? beforeVersion;
413
+ const refreshedComparison = refreshedVersion ? compareSemver(currentVersion, refreshedVersion) : void 0;
414
+ if (refreshedComparison !== void 0 && refreshedComparison < 0) {
415
+ runClaude(["plugin", "update", CLAUDE_PLUGIN], log, true);
416
+ } else {
417
+ log(
418
+ `${CLAUDE_PLUGIN} ${currentVersion} was not updated because the refreshed marketplace does not advertise a newer semantic version.`
419
+ );
420
+ }
421
+ }
422
+ }
423
+ const afterVersion = installedPluginVersion(configDir2);
424
+ const change = beforeVersion && afterVersion ? compareSemver(afterVersion, beforeVersion) : void 0;
425
+ if (change !== void 0 && change < 0) {
426
+ log(`WARNING: ${CLAUDE_PLUGIN} resolved to ${afterVersion}, below its prior ${beforeVersion}.`);
427
+ }
428
+ return {
429
+ beforeVersion,
430
+ afterVersion,
431
+ action: !afterVersion ? "unverified" : !beforeVersion ? "installed" : change !== void 0 && change > 0 ? "updated" : "unchanged"
432
+ };
433
+ }
434
+ function supportsAuthenticatedWhoami(version = PACKAGE_VERSION) {
435
+ return (compareSemver(version, AUTH_VERIFY_MIN_VERSION) ?? -1) >= 0;
436
+ }
437
+ function authenticatedWhoamiGuidance(version = PACKAGE_VERSION) {
438
+ if (!supportsAuthenticatedWhoami(version)) return [];
439
+ return [
440
+ `Verify the authenticated workspace with: npx --yes --package ${PACKAGE_NAME} impractical whoami`,
441
+ "A healthy result prints your org and plan; MCP \u201CConnected\u201D alone only proves startup."
442
+ ];
443
+ }
444
+ function pluginResultLine(result) {
445
+ if (!result.afterVersion) {
446
+ return `${CLAUDE_PLUGIN} setup completed, but Claude's resolved version could not be read back.`;
447
+ }
448
+ if (result.action === "installed") {
449
+ return `Installed ${CLAUDE_PLUGIN} ${result.afterVersion} for Claude Code (user scope).`;
450
+ }
451
+ if (result.action === "updated") {
452
+ return `Updated ${CLAUDE_PLUGIN} ${result.beforeVersion} \u2192 ${result.afterVersion} for Claude Code (user scope).`;
453
+ }
454
+ return `${CLAUDE_PLUGIN} already at ${result.afterVersion}; left unchanged.`;
455
+ }
456
+ var skillSourcePath = () => fileURLToPath(new URL("../assets/impractical/SKILL.md", import.meta.url));
457
+ function installSkills(home = homedir2(), sourcePath = skillSourcePath()) {
458
+ const source = readFileSync2(sourcePath, "utf8");
459
+ const targets = [
460
+ {
461
+ harness: "Claude Code",
462
+ path: join2(home, ".claude", "skills", "impractical", "SKILL.md")
463
+ },
464
+ {
465
+ harness: "Codex",
466
+ path: join2(home, ".codex", "skills", "impractical", "SKILL.md")
467
+ }
468
+ ];
469
+ for (const target of targets) {
470
+ mkdirSync2(dirname(target.path), { recursive: true, mode: 448 });
471
+ writeFileSync2(target.path, source, { mode: 420 });
472
+ }
473
+ return targets;
474
+ }
475
+ function reconcileLegacySkills(home = homedir2(), sourcePath = skillSourcePath()) {
476
+ const source = readFileSync2(sourcePath, "utf8");
477
+ const targets = [
478
+ {
479
+ harness: "Claude Code",
480
+ path: join2(home, ".claude", "skills", "impractical", "SKILL.md")
481
+ },
482
+ {
483
+ harness: "Codex",
484
+ path: join2(home, ".codex", "skills", "impractical", "SKILL.md")
485
+ }
486
+ ];
487
+ const reconciled = [];
488
+ for (const target of targets) {
489
+ if (!existsSync2(target.path)) continue;
490
+ const previous = readFileSync2(target.path, "utf8");
491
+ if (previous === source) continue;
492
+ const backupPath = `${target.path}.pre-plugin`;
493
+ if (!existsSync2(backupPath)) writeFileSync2(backupPath, previous, { mode: 420 });
494
+ writeFileSync2(target.path, source, { mode: 420 });
495
+ reconciled.push({ ...target, backupPath });
496
+ }
497
+ return reconciled;
498
+ }
499
+ function browserCommand(url) {
500
+ if (process.platform === "darwin") return { command: "open", args: [url] };
501
+ if (process.platform === "win32") return { command: "cmd", args: ["/c", "start", "", url] };
502
+ return { command: "xdg-open", args: [url] };
503
+ }
504
+ function openBrowser(url) {
505
+ const launch = browserCommand(url);
506
+ const result = spawnSync(launch.command, launch.args, {
507
+ stdio: "ignore",
508
+ windowsHide: true
509
+ });
510
+ return !result.error && result.status === 0;
511
+ }
512
+ async function waitForApproval(supabaseUrl, authorization, log) {
513
+ const deadline = Date.parse(authorization.expiresAt);
514
+ const intervalMs = Math.max(1, authorization.intervalSeconds) * 1e3;
515
+ let announced = false;
516
+ while (Date.now() < deadline) {
517
+ const result = await invokeAt(supabaseUrl, "cli-device-auth", {
518
+ action: "poll",
519
+ deviceCode: authorization.deviceCode
520
+ });
521
+ if (result.status === "authorized") return result.apiKey;
522
+ if (!announced) {
523
+ log("Waiting for browser approval\u2026");
524
+ announced = true;
525
+ }
526
+ await delay(intervalMs);
527
+ }
528
+ throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup --claude\` again.`);
529
+ }
530
+ async function setup(options) {
531
+ if (options.installClaudePlugin) verifyClaudePrerequisites();
532
+ const corruptConfigBackup = recoverCorruptConfig();
533
+ if (corruptConfigBackup) {
534
+ options.log(`Backed up invalid configuration to ${corruptConfigBackup}; starting clean.`);
535
+ }
536
+ const supabaseUrl = process.env.IMPRACTICAL_SUPABASE_URL?.trim() || PRODUCTION_SUPABASE_URL;
537
+ const appUrl = process.env.IMPRACTICAL_APP_URL?.trim() || PRODUCTION_APP_URL;
538
+ const existing = loadConfig();
539
+ let apiKey = existing.supabaseUrl === supabaseUrl ? existing.apiKey : void 0;
540
+ let verified;
541
+ if (apiKey) {
542
+ try {
543
+ verified = await invokeAt(supabaseUrl, "workspace-bootstrap", {}, apiKey);
544
+ options.log("Existing connection verified; browser authorization is not needed.");
545
+ } catch (error) {
546
+ if (!(error instanceof ApiError) || error.status !== 401) throw error;
547
+ apiKey = void 0;
548
+ }
549
+ }
550
+ if (!apiKey) {
551
+ const authorization = await invokeAt(supabaseUrl, "cli-device-auth", {
552
+ action: "start"
553
+ });
554
+ options.log("");
555
+ options.log("Connect Impractical Motion");
556
+ options.log(`Code: ${authorization.userCode}`);
557
+ options.log(`Open: ${authorization.verificationUri}`);
558
+ options.log("");
559
+ if (options.openBrowser) {
560
+ const opened = openBrowser(authorization.verificationUri);
561
+ options.log(opened ? "Opened your browser." : "Could not open a browser automatically; use the link above.");
562
+ }
563
+ try {
564
+ apiKey = await waitForApproval(supabaseUrl, authorization, options.log);
565
+ } catch (error) {
566
+ if (error instanceof ApiError && error.status === 410) {
567
+ throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup --claude\` again.`);
568
+ }
569
+ throw error;
570
+ }
571
+ verified = await invokeAt(supabaseUrl, "workspace-bootstrap", {}, apiKey);
572
+ saveConfig({ apiKey, supabaseUrl, appUrl });
573
+ }
574
+ const claudePlugin = options.installClaudePlugin ? installClaudePlugin(options.log) : void 0;
575
+ const installed = options.installAgentSkills ? installSkills() : [];
576
+ const reconciled = reconcileLegacySkills();
577
+ options.log("");
578
+ options.log("Connected.");
579
+ for (const skill of installed) {
580
+ options.log(`Installed /impractical for ${skill.harness}.`);
581
+ }
582
+ for (const skill of reconciled) {
583
+ options.log(`Refreshed legacy /impractical for ${skill.harness}; previous copy preserved at ${skill.backupPath}.`);
584
+ }
585
+ if (claudePlugin) options.log(pluginResultLine(claudePlugin));
586
+ options.log(`Workspace: ${verified.orgId}${verified.plan ? ` \xB7 ${verified.plan}` : ""}`);
587
+ options.log("");
588
+ options.log("Run /reload-plugins, then type continue.");
589
+ if (options.installClaudePlugin) {
590
+ for (const line of authenticatedWhoamiGuidance()) options.log(line);
591
+ }
592
+ return { installed, appUrl, supabaseUrl };
593
+ }
594
+
595
+ 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
+ openBrowser,
616
+ setup
617
+ };