@nicmeriano/spool 0.0.0-preview.1741721
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/dist/cli.js +376 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/vite.d.ts +14 -0
- package/dist/vite.js +70 -0
- package/dist/vite.js.map +1 -0
- package/package.json +67 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/init.ts
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as readline from "readline";
|
|
7
|
+
|
|
8
|
+
// src/utils/log.ts
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
var PREFIX = pc.magenta("spool");
|
|
11
|
+
var log = {
|
|
12
|
+
info(msg) {
|
|
13
|
+
console.log(` ${PREFIX} ${msg}`);
|
|
14
|
+
},
|
|
15
|
+
success(msg) {
|
|
16
|
+
console.log(` ${PREFIX} ${pc.green(msg)}`);
|
|
17
|
+
},
|
|
18
|
+
warn(msg) {
|
|
19
|
+
console.warn(` ${PREFIX} ${pc.yellow(msg)}`);
|
|
20
|
+
},
|
|
21
|
+
error(msg) {
|
|
22
|
+
console.error(` ${PREFIX} ${pc.red(msg)}`);
|
|
23
|
+
},
|
|
24
|
+
banner(port) {
|
|
25
|
+
const url = `http://localhost:${port}`;
|
|
26
|
+
const visibleLine = `Spool: ${url}`;
|
|
27
|
+
const coloredLine = `Spool: ${pc.cyan(url)}`;
|
|
28
|
+
const innerWidth = 40;
|
|
29
|
+
const contentWidth = visibleLine.length;
|
|
30
|
+
const leftPad = 3;
|
|
31
|
+
const rightPad = innerWidth - leftPad - contentWidth;
|
|
32
|
+
const h = "\u2500".repeat(innerWidth);
|
|
33
|
+
const empty = "\u2502" + " ".repeat(innerWidth) + "\u2502";
|
|
34
|
+
const row = "\u2502" + " ".repeat(leftPad) + coloredLine + " ".repeat(rightPad) + "\u2502";
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log(` \u250C${h}\u2510`);
|
|
37
|
+
console.log(` ${empty}`);
|
|
38
|
+
console.log(` ${row}`);
|
|
39
|
+
console.log(` ${empty}`);
|
|
40
|
+
console.log(` \u2514${h}\u2518`);
|
|
41
|
+
console.log("");
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/commands/init.ts
|
|
46
|
+
import pc2 from "picocolors";
|
|
47
|
+
function detectFramework(cwd) {
|
|
48
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
49
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
50
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
51
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
52
|
+
if (deps["next"]) {
|
|
53
|
+
return { name: "Next.js", devCommand: "npm run dev", usesVite: false };
|
|
54
|
+
}
|
|
55
|
+
if (deps["vite"]) {
|
|
56
|
+
return { name: "Vite", devCommand: "npm run dev", usesVite: true };
|
|
57
|
+
}
|
|
58
|
+
if (deps["react-scripts"]) {
|
|
59
|
+
return { name: "Create React App", devCommand: "npm start", usesVite: false };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function findViteConfig(cwd) {
|
|
64
|
+
const candidates = ["vite.config.ts", "vite.config.js", "vite.config.mjs"];
|
|
65
|
+
for (const name of candidates) {
|
|
66
|
+
const full = path.join(cwd, name);
|
|
67
|
+
if (fs.existsSync(full)) return full;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function addVitePlugin(configPath) {
|
|
72
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
73
|
+
if (content.includes("@nicmeriano/spool/vite") || content.includes("spool()")) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const importRegex = /^import\s.+$/gm;
|
|
77
|
+
let lastImportMatch = null;
|
|
78
|
+
let match;
|
|
79
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
80
|
+
lastImportMatch = match;
|
|
81
|
+
}
|
|
82
|
+
if (!lastImportMatch) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const importInsertPos = lastImportMatch.index + lastImportMatch[0].length;
|
|
86
|
+
const spoolImport = `
|
|
87
|
+
import { spool } from '@nicmeriano/spool/vite'`;
|
|
88
|
+
const pluginsRegex = /plugins\s*:\s*\[/;
|
|
89
|
+
const pluginsMatch = pluginsRegex.exec(content);
|
|
90
|
+
if (!pluginsMatch) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
let newContent = content.slice(0, importInsertPos) + spoolImport + content.slice(importInsertPos);
|
|
94
|
+
const offset = spoolImport.length;
|
|
95
|
+
const pluginsInsertPos = pluginsMatch.index + pluginsMatch[0].length + offset;
|
|
96
|
+
newContent = newContent.slice(0, pluginsInsertPos) + "spool(), " + newContent.slice(pluginsInsertPos);
|
|
97
|
+
fs.writeFileSync(configPath, newContent, "utf-8");
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
function prompt(question, defaultValue) {
|
|
101
|
+
const rl = readline.createInterface({
|
|
102
|
+
input: process.stdin,
|
|
103
|
+
output: process.stdout
|
|
104
|
+
});
|
|
105
|
+
const displayDefault = defaultValue ? ` (${defaultValue})` : "";
|
|
106
|
+
return new Promise((resolve3) => {
|
|
107
|
+
rl.question(`${question}${displayDefault}: `, (answer) => {
|
|
108
|
+
rl.close();
|
|
109
|
+
resolve3(answer.trim() || defaultValue || "");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function runInit(cwd) {
|
|
114
|
+
log.info("Initializing Spool...");
|
|
115
|
+
console.log("");
|
|
116
|
+
const configPath = path.join(cwd, "spool.config.ts");
|
|
117
|
+
if (fs.existsSync(configPath)) {
|
|
118
|
+
log.info("spool.config.ts already exists.");
|
|
119
|
+
const framework2 = detectFramework(cwd);
|
|
120
|
+
if (framework2?.usesVite) {
|
|
121
|
+
const viteConfigPath = findViteConfig(cwd);
|
|
122
|
+
if (viteConfigPath) {
|
|
123
|
+
const viteContent = fs.readFileSync(viteConfigPath, "utf-8");
|
|
124
|
+
if (!viteContent.includes("@nicmeriano/spool/vite") && !viteContent.includes("spool()")) {
|
|
125
|
+
const configName = path.basename(viteConfigPath);
|
|
126
|
+
const success = addVitePlugin(viteConfigPath);
|
|
127
|
+
if (success) {
|
|
128
|
+
log.success(`Added spool plugin to ${configName}`);
|
|
129
|
+
} else {
|
|
130
|
+
log.warn(`Could not automatically modify ${configName}.`);
|
|
131
|
+
console.log(" Please add the spool plugin manually:\n");
|
|
132
|
+
console.log(" import { spool } from '@nicmeriano/spool/vite'\n");
|
|
133
|
+
console.log(" // In your plugins array:");
|
|
134
|
+
console.log(" plugins: [spool(), ...otherPlugins]\n");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log("");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const framework = detectFramework(cwd);
|
|
143
|
+
if (framework) {
|
|
144
|
+
log.info(`Detected framework: ${pc2.bold(framework.name)}`);
|
|
145
|
+
console.log("");
|
|
146
|
+
}
|
|
147
|
+
const defaultDevCmd = framework?.devCommand || "npm run dev";
|
|
148
|
+
const devCommand = await prompt("Dev command", defaultDevCmd);
|
|
149
|
+
const configContent = `import { defineConfig } from '@nicmeriano/spool';
|
|
150
|
+
|
|
151
|
+
export default defineConfig({
|
|
152
|
+
devCommand: '${devCommand}',
|
|
153
|
+
});
|
|
154
|
+
`;
|
|
155
|
+
fs.writeFileSync(configPath, configContent, "utf-8");
|
|
156
|
+
console.log("");
|
|
157
|
+
log.success("Created spool.config.ts");
|
|
158
|
+
const spoolDir = path.join(cwd, ".spool");
|
|
159
|
+
if (!fs.existsSync(spoolDir)) {
|
|
160
|
+
fs.mkdirSync(spoolDir, { recursive: true });
|
|
161
|
+
log.success("Created .spool/ directory");
|
|
162
|
+
}
|
|
163
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
164
|
+
if (fs.existsSync(gitignorePath)) {
|
|
165
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
166
|
+
if (!content.includes(".spool")) {
|
|
167
|
+
fs.appendFileSync(gitignorePath, "\n.spool/\n");
|
|
168
|
+
log.success("Added .spool/ to .gitignore");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (framework?.usesVite) {
|
|
172
|
+
const viteConfigPath = findViteConfig(cwd);
|
|
173
|
+
if (viteConfigPath) {
|
|
174
|
+
const configName = path.basename(viteConfigPath);
|
|
175
|
+
const success = addVitePlugin(viteConfigPath);
|
|
176
|
+
if (success) {
|
|
177
|
+
log.success(`Added spool plugin to ${configName}`);
|
|
178
|
+
} else {
|
|
179
|
+
log.warn(`Could not automatically modify ${configName}.`);
|
|
180
|
+
console.log(" Please add the spool plugin manually:\n");
|
|
181
|
+
console.log(" import { spool } from '@nicmeriano/spool/vite'\n");
|
|
182
|
+
console.log(" // In your plugins array:");
|
|
183
|
+
console.log(" plugins: [spool(), ...otherPlugins]\n");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
log.info("Add this script tag to your HTML:");
|
|
188
|
+
console.log(' <script src="http://localhost:3142/inject.js"></script>');
|
|
189
|
+
}
|
|
190
|
+
console.log("");
|
|
191
|
+
log.success(`Done! Run ${pc2.cyan('"spool open"')} to start.`);
|
|
192
|
+
console.log("");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/commands/open.ts
|
|
196
|
+
import { spawn } from "child_process";
|
|
197
|
+
import * as fs3 from "fs";
|
|
198
|
+
import * as path3 from "path";
|
|
199
|
+
|
|
200
|
+
// src/utils/load-config.ts
|
|
201
|
+
import * as fs2 from "fs";
|
|
202
|
+
import * as path2 from "path";
|
|
203
|
+
async function loadConfig(cwd) {
|
|
204
|
+
const configPath = path2.resolve(cwd, "spool.config.ts");
|
|
205
|
+
if (!fs2.existsSync(configPath)) {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
const { createJiti } = await import("jiti");
|
|
209
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
210
|
+
const config = await jiti.import(configPath);
|
|
211
|
+
return config.default ?? config;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/utils/port.ts
|
|
215
|
+
import * as net from "net";
|
|
216
|
+
async function findAvailablePort(preferred) {
|
|
217
|
+
return new Promise((resolve3, reject) => {
|
|
218
|
+
const server = net.createServer();
|
|
219
|
+
server.listen(preferred, () => {
|
|
220
|
+
server.close(() => resolve3(preferred));
|
|
221
|
+
});
|
|
222
|
+
server.on("error", (err) => {
|
|
223
|
+
if (err.code === "EADDRINUSE") {
|
|
224
|
+
findAvailablePort(preferred + 1).then(resolve3).catch(reject);
|
|
225
|
+
} else {
|
|
226
|
+
reject(err);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/utils/browser.ts
|
|
233
|
+
import { exec } from "child_process";
|
|
234
|
+
function openBrowser(url) {
|
|
235
|
+
const platform = process.platform;
|
|
236
|
+
let command2;
|
|
237
|
+
if (platform === "darwin") {
|
|
238
|
+
command2 = `open "${url}"`;
|
|
239
|
+
} else if (platform === "win32") {
|
|
240
|
+
command2 = `start "${url}"`;
|
|
241
|
+
} else {
|
|
242
|
+
command2 = `xdg-open "${url}"`;
|
|
243
|
+
}
|
|
244
|
+
exec(command2, (error) => {
|
|
245
|
+
if (error) {
|
|
246
|
+
console.error(`Could not open browser: ${error.message}`);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/constants.ts
|
|
252
|
+
var DEFAULT_SERVER_PORT = 3142;
|
|
253
|
+
|
|
254
|
+
// src/commands/open.ts
|
|
255
|
+
import pc3 from "picocolors";
|
|
256
|
+
function detectDevCommand(cwd) {
|
|
257
|
+
const pkgPath = path3.resolve(cwd, "package.json");
|
|
258
|
+
if (!fs3.existsSync(pkgPath)) return void 0;
|
|
259
|
+
try {
|
|
260
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
261
|
+
if (pkg.scripts?.dev) {
|
|
262
|
+
return "npm run dev";
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
return void 0;
|
|
267
|
+
}
|
|
268
|
+
async function runOpen(cwd, options = {}) {
|
|
269
|
+
const config = await loadConfig(cwd);
|
|
270
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
271
|
+
log.warn("ANTHROPIC_API_KEY is not set. Claude features will not work.");
|
|
272
|
+
}
|
|
273
|
+
const preferredPort = options.port ?? config.port ?? DEFAULT_SERVER_PORT;
|
|
274
|
+
const port = await findAvailablePort(preferredPort);
|
|
275
|
+
if (port !== preferredPort) {
|
|
276
|
+
log.info(`Port ${preferredPort} is in use, using ${pc3.bold(String(port))} instead.`);
|
|
277
|
+
}
|
|
278
|
+
console.log("");
|
|
279
|
+
log.info("Starting dev servers...");
|
|
280
|
+
const { startServer } = await import("@nicmeriano/spool-server");
|
|
281
|
+
await startServer({ port, cwd });
|
|
282
|
+
const devCommand = config.devCommand ?? detectDevCommand(cwd);
|
|
283
|
+
if (devCommand) {
|
|
284
|
+
const [cmd, ...args2] = devCommand.split(" ");
|
|
285
|
+
const logDir = path3.resolve(cwd, ".spool");
|
|
286
|
+
if (!fs3.existsSync(logDir)) {
|
|
287
|
+
fs3.mkdirSync(logDir, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
const logFile = path3.resolve(logDir, "dev.log");
|
|
290
|
+
let child;
|
|
291
|
+
if (options.verbose) {
|
|
292
|
+
child = spawn(cmd, args2, {
|
|
293
|
+
cwd,
|
|
294
|
+
stdio: "inherit",
|
|
295
|
+
shell: true
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
const logFd = fs3.openSync(logFile, "w");
|
|
299
|
+
child = spawn(cmd, args2, {
|
|
300
|
+
cwd,
|
|
301
|
+
stdio: ["ignore", logFd, logFd],
|
|
302
|
+
shell: true
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
child.on("error", (error) => {
|
|
306
|
+
log.error(`Dev server error: ${error.message}`);
|
|
307
|
+
});
|
|
308
|
+
process.on("SIGINT", () => {
|
|
309
|
+
child.kill();
|
|
310
|
+
process.exit(0);
|
|
311
|
+
});
|
|
312
|
+
process.on("SIGTERM", () => {
|
|
313
|
+
child.kill();
|
|
314
|
+
process.exit(0);
|
|
315
|
+
});
|
|
316
|
+
} else {
|
|
317
|
+
log.warn('No dev command found. Add devCommand to spool.config.ts or a "dev" script to package.json.');
|
|
318
|
+
}
|
|
319
|
+
log.banner(port);
|
|
320
|
+
if (!options.noOpen) {
|
|
321
|
+
openBrowser(`http://localhost:${port}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/cli.ts
|
|
326
|
+
var args = process.argv.slice(2);
|
|
327
|
+
var command = args[0];
|
|
328
|
+
function printUsage() {
|
|
329
|
+
console.log(`
|
|
330
|
+
Usage: spool <command> [options]
|
|
331
|
+
|
|
332
|
+
Commands:
|
|
333
|
+
init Initialize spool in your project
|
|
334
|
+
open Start spool server and open the UI
|
|
335
|
+
|
|
336
|
+
Options (open):
|
|
337
|
+
--port <n> Server port (default: 3142)
|
|
338
|
+
--no-open Don't open browser automatically
|
|
339
|
+
--verbose Show dev server output in terminal
|
|
340
|
+
|
|
341
|
+
Examples:
|
|
342
|
+
spool init
|
|
343
|
+
spool open
|
|
344
|
+
spool open --port 4000
|
|
345
|
+
`);
|
|
346
|
+
}
|
|
347
|
+
async function main() {
|
|
348
|
+
const cwd = process.cwd();
|
|
349
|
+
switch (command) {
|
|
350
|
+
case "init":
|
|
351
|
+
await runInit(cwd);
|
|
352
|
+
break;
|
|
353
|
+
case "open": {
|
|
354
|
+
const portIndex = args.indexOf("--port");
|
|
355
|
+
const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : void 0;
|
|
356
|
+
const noOpen = args.includes("--no-open");
|
|
357
|
+
const verbose = args.includes("--verbose");
|
|
358
|
+
await runOpen(cwd, { port, noOpen, verbose });
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case "--help":
|
|
362
|
+
case "-h":
|
|
363
|
+
case void 0:
|
|
364
|
+
printUsage();
|
|
365
|
+
break;
|
|
366
|
+
default:
|
|
367
|
+
console.error(`Unknown command: ${command}`);
|
|
368
|
+
printUsage();
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
main().catch((error) => {
|
|
373
|
+
console.error(error.message || error);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
});
|
|
376
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/init.ts","../src/utils/log.ts","../src/commands/open.ts","../src/utils/load-config.ts","../src/utils/port.ts","../src/utils/browser.ts","../src/constants.ts","../src/cli.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as readline from 'node:readline';\nimport { log } from '../utils/log.js';\nimport pc from 'picocolors';\n\n/**\n * Detect the project framework\n */\nfunction detectFramework(cwd: string): { name: string; devCommand: string; usesVite: boolean } | null {\n const pkgPath = path.join(cwd, 'package.json');\n if (!fs.existsSync(pkgPath)) return null;\n\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n if (deps['next']) {\n return { name: 'Next.js', devCommand: 'npm run dev', usesVite: false };\n }\n if (deps['vite']) {\n return { name: 'Vite', devCommand: 'npm run dev', usesVite: true };\n }\n if (deps['react-scripts']) {\n return { name: 'Create React App', devCommand: 'npm start', usesVite: false };\n }\n\n return null;\n}\n\n/**\n * Find the Vite config file in the project\n */\nfunction findViteConfig(cwd: string): string | null {\n const candidates = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'];\n for (const name of candidates) {\n const full = path.join(cwd, name);\n if (fs.existsSync(full)) return full;\n }\n return null;\n}\n\n/**\n * Add the spool Vite plugin to an existing vite config file.\n * Returns true if successful, false if the config couldn't be modified automatically.\n */\nfunction addVitePlugin(configPath: string): boolean {\n const content = fs.readFileSync(configPath, 'utf-8');\n\n // Skip if already configured\n if (content.includes('@nicmeriano/spool/vite') || content.includes('spool()')) {\n return true;\n }\n\n // Find the last import statement to insert our import after it\n const importRegex = /^import\\s.+$/gm;\n let lastImportMatch: RegExpExecArray | null = null;\n let match: RegExpExecArray | null;\n while ((match = importRegex.exec(content)) !== null) {\n lastImportMatch = match;\n }\n\n if (!lastImportMatch) {\n return false;\n }\n\n const importInsertPos = lastImportMatch.index + lastImportMatch[0].length;\n const spoolImport = `\\nimport { spool } from '@nicmeriano/spool/vite'`;\n\n // Find the plugins array and add spool() to it\n const pluginsRegex = /plugins\\s*:\\s*\\[/;\n const pluginsMatch = pluginsRegex.exec(content);\n\n if (!pluginsMatch) {\n return false;\n }\n\n // Build the new content: add import, then add spool() to plugins\n let newContent = content.slice(0, importInsertPos) + spoolImport + content.slice(importInsertPos);\n\n // Re-find plugins position (shifted by the import we added)\n const offset = spoolImport.length;\n const pluginsInsertPos = pluginsMatch.index + pluginsMatch[0].length + offset;\n newContent = newContent.slice(0, pluginsInsertPos) + 'spool(), ' + newContent.slice(pluginsInsertPos);\n\n fs.writeFileSync(configPath, newContent, 'utf-8');\n return true;\n}\n\n/**\n * Prompt the user for input\n */\nfunction prompt(question: string, defaultValue?: string): Promise<string> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const displayDefault = defaultValue ? ` (${defaultValue})` : '';\n\n return new Promise((resolve) => {\n rl.question(`${question}${displayDefault}: `, (answer) => {\n rl.close();\n resolve(answer.trim() || defaultValue || '');\n });\n });\n}\n\n/**\n * Run the spool init command\n */\nexport async function runInit(cwd: string): Promise<void> {\n log.info('Initializing Spool...');\n console.log('');\n\n // Check if config already exists\n const configPath = path.join(cwd, 'spool.config.ts');\n if (fs.existsSync(configPath)) {\n log.info('spool.config.ts already exists.');\n\n // Still check if the Vite plugin needs to be added\n const framework = detectFramework(cwd);\n if (framework?.usesVite) {\n const viteConfigPath = findViteConfig(cwd);\n if (viteConfigPath) {\n const viteContent = fs.readFileSync(viteConfigPath, 'utf-8');\n if (!viteContent.includes('@nicmeriano/spool/vite') && !viteContent.includes('spool()')) {\n const configName = path.basename(viteConfigPath);\n const success = addVitePlugin(viteConfigPath);\n if (success) {\n log.success(`Added spool plugin to ${configName}`);\n } else {\n log.warn(`Could not automatically modify ${configName}.`);\n console.log(' Please add the spool plugin manually:\\n');\n console.log(' import { spool } from \\'@nicmeriano/spool/vite\\'\\n');\n console.log(' // In your plugins array:');\n console.log(' plugins: [spool(), ...otherPlugins]\\n');\n }\n }\n }\n }\n\n console.log('');\n return;\n }\n\n // Detect framework\n const framework = detectFramework(cwd);\n if (framework) {\n log.info(`Detected framework: ${pc.bold(framework.name)}`);\n console.log('');\n }\n\n // Prompt for dev command\n const defaultDevCmd = framework?.devCommand || 'npm run dev';\n const devCommand = await prompt('Dev command', defaultDevCmd);\n\n // Write spool.config.ts\n const configContent = `import { defineConfig } from '@nicmeriano/spool';\n\nexport default defineConfig({\n devCommand: '${devCommand}',\n});\n`;\n\n fs.writeFileSync(configPath, configContent, 'utf-8');\n console.log('');\n log.success('Created spool.config.ts');\n\n // Create .spool directory\n const spoolDir = path.join(cwd, '.spool');\n if (!fs.existsSync(spoolDir)) {\n fs.mkdirSync(spoolDir, { recursive: true });\n log.success('Created .spool/ directory');\n }\n\n // Add .spool/ to .gitignore\n const gitignorePath = path.join(cwd, '.gitignore');\n if (fs.existsSync(gitignorePath)) {\n const content = fs.readFileSync(gitignorePath, 'utf-8');\n if (!content.includes('.spool')) {\n fs.appendFileSync(gitignorePath, '\\n.spool/\\n');\n log.success('Added .spool/ to .gitignore');\n }\n }\n\n // Auto-configure Vite plugin if this is a Vite project\n if (framework?.usesVite) {\n const viteConfigPath = findViteConfig(cwd);\n if (viteConfigPath) {\n const configName = path.basename(viteConfigPath);\n const success = addVitePlugin(viteConfigPath);\n if (success) {\n log.success(`Added spool plugin to ${configName}`);\n } else {\n log.warn(`Could not automatically modify ${configName}.`);\n console.log(' Please add the spool plugin manually:\\n');\n console.log(' import { spool } from \\'@nicmeriano/spool/vite\\'\\n');\n console.log(' // In your plugins array:');\n console.log(' plugins: [spool(), ...otherPlugins]\\n');\n }\n }\n } else {\n log.info('Add this script tag to your HTML:');\n console.log(' <script src=\"http://localhost:3142/inject.js\"></script>');\n }\n\n console.log('');\n log.success(`Done! Run ${pc.cyan('\"spool open\"')} to start.`);\n console.log('');\n}\n","import pc from 'picocolors';\n\nconst PREFIX = pc.magenta('spool');\n\nexport const log = {\n info(msg: string) {\n console.log(` ${PREFIX} ${msg}`);\n },\n success(msg: string) {\n console.log(` ${PREFIX} ${pc.green(msg)}`);\n },\n warn(msg: string) {\n console.warn(` ${PREFIX} ${pc.yellow(msg)}`);\n },\n error(msg: string) {\n console.error(` ${PREFIX} ${pc.red(msg)}`);\n },\n banner(port: number) {\n const url = `http://localhost:${port}`;\n const visibleLine = `Spool: ${url}`;\n const coloredLine = `Spool: ${pc.cyan(url)}`;\n\n // Box inner width = 3 (left pad) + content + right pad + 0 (border chars separate)\n const innerWidth = 40;\n const contentWidth = visibleLine.length;\n const leftPad = 3;\n const rightPad = innerWidth - leftPad - contentWidth;\n\n const h = '\\u2500'.repeat(innerWidth);\n const empty = '\\u2502' + ' '.repeat(innerWidth) + '\\u2502';\n const row = '\\u2502' + ' '.repeat(leftPad) + coloredLine + ' '.repeat(rightPad) + '\\u2502';\n\n console.log('');\n console.log(` \\u250c${h}\\u2510`);\n console.log(` ${empty}`);\n console.log(` ${row}`);\n console.log(` ${empty}`);\n console.log(` \\u2514${h}\\u2518`);\n console.log('');\n },\n};\n","import { spawn } from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { loadConfig } from '../utils/load-config.js';\nimport { findAvailablePort } from '../utils/port.js';\nimport { openBrowser } from '../utils/browser.js';\nimport { DEFAULT_SERVER_PORT } from '../constants.js';\nimport { log } from '../utils/log.js';\nimport pc from 'picocolors';\n\nexport interface OpenOptions {\n port?: number;\n noOpen?: boolean;\n verbose?: boolean;\n}\n\n/**\n * Auto-detect dev command from package.json scripts.dev\n */\nfunction detectDevCommand(cwd: string): string | undefined {\n const pkgPath = path.resolve(cwd, 'package.json');\n if (!fs.existsSync(pkgPath)) return undefined;\n\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n if (pkg.scripts?.dev) {\n return 'npm run dev';\n }\n } catch {\n // Ignore parse errors\n }\n return undefined;\n}\n\n/**\n * Run the spool open command\n */\nexport async function runOpen(cwd: string, options: OpenOptions = {}): Promise<void> {\n // Load config (returns defaults if no spool.config.ts)\n const config = await loadConfig(cwd);\n\n // Check for ANTHROPIC_API_KEY\n if (!process.env.ANTHROPIC_API_KEY) {\n log.warn('ANTHROPIC_API_KEY is not set. Claude features will not work.');\n }\n\n // Find available port\n const preferredPort = options.port ?? config.port ?? DEFAULT_SERVER_PORT;\n const port = await findAvailablePort(preferredPort);\n if (port !== preferredPort) {\n log.info(`Port ${preferredPort} is in use, using ${pc.bold(String(port))} instead.`);\n }\n\n console.log('');\n log.info('Starting dev servers...');\n\n // Start the server\n const { startServer } = await import('@nicmeriano/spool-server');\n\n // Start server — resolves once the HTTP server is listening\n await startServer({ port, cwd });\n\n // Resolve dev command: config > auto-detect from package.json > none\n const devCommand = config.devCommand ?? detectDevCommand(cwd);\n\n // Spawn dev command\n if (devCommand) {\n const [cmd, ...args] = devCommand.split(' ');\n\n // Default: pipe output to log file for clean terminal\n const logDir = path.resolve(cwd, '.spool');\n if (!fs.existsSync(logDir)) {\n fs.mkdirSync(logDir, { recursive: true });\n }\n const logFile = path.resolve(logDir, 'dev.log');\n\n let child;\n if (options.verbose) {\n child = spawn(cmd, args, {\n cwd,\n stdio: 'inherit',\n shell: true,\n });\n } else {\n const logFd = fs.openSync(logFile, 'w');\n child = spawn(cmd, args, {\n cwd,\n stdio: ['ignore', logFd, logFd],\n shell: true,\n });\n }\n\n child.on('error', (error) => {\n log.error(`Dev server error: ${error.message}`);\n });\n\n // Clean up on exit\n process.on('SIGINT', () => {\n child.kill();\n process.exit(0);\n });\n process.on('SIGTERM', () => {\n child.kill();\n process.exit(0);\n });\n } else {\n log.warn('No dev command found. Add devCommand to spool.config.ts or a \"dev\" script to package.json.');\n }\n\n // Show banner and open browser — server is already listening\n log.banner(port);\n\n if (!options.noOpen) {\n openBrowser(`http://localhost:${port}`);\n }\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * Load spool.config.ts from the project root\n */\nexport async function loadConfig(cwd: string): Promise<import('../config.js').SpoolConfig> {\n const configPath = path.resolve(cwd, 'spool.config.ts');\n\n if (!fs.existsSync(configPath)) {\n return {};\n }\n\n // Use jiti for TypeScript config loading\n const { createJiti } = await import('jiti');\n const jiti = createJiti(import.meta.url, { interopDefault: true });\n const config = await jiti.import(configPath) as { default?: import('../config.js').SpoolConfig } & import('../config.js').SpoolConfig;\n\n return config.default ?? config;\n}\n","import * as net from 'node:net';\n\n/**\n * Find an available port, starting from the preferred port\n */\nexport async function findAvailablePort(preferred: number): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n server.listen(preferred, () => {\n server.close(() => resolve(preferred));\n });\n server.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n // Try next port\n findAvailablePort(preferred + 1).then(resolve).catch(reject);\n } else {\n reject(err);\n }\n });\n });\n}\n","import { exec } from 'node:child_process';\n\n/**\n * Open a URL in the default browser\n */\nexport function openBrowser(url: string): void {\n const platform = process.platform;\n let command: string;\n\n if (platform === 'darwin') {\n command = `open \"${url}\"`;\n } else if (platform === 'win32') {\n command = `start \"${url}\"`;\n } else {\n command = `xdg-open \"${url}\"`;\n }\n\n exec(command, (error) => {\n if (error) {\n console.error(`Could not open browser: ${error.message}`);\n }\n });\n}\n","export const DEFAULT_SERVER_PORT = 3142;\nexport const DEFAULT_APP_PORT = 5173;\n","import { runInit } from './commands/init.js';\nimport { runOpen } from './commands/open.js';\n\nconst args = process.argv.slice(2);\nconst command = args[0];\n\nfunction printUsage(): void {\n console.log(`\nUsage: spool <command> [options]\n\nCommands:\n init Initialize spool in your project\n open Start spool server and open the UI\n\nOptions (open):\n --port <n> Server port (default: 3142)\n --no-open Don't open browser automatically\n --verbose Show dev server output in terminal\n\nExamples:\n spool init\n spool open\n spool open --port 4000\n`);\n}\n\nasync function main(): Promise<void> {\n const cwd = process.cwd();\n\n switch (command) {\n case 'init':\n await runInit(cwd);\n break;\n\n case 'open': {\n const portIndex = args.indexOf('--port');\n const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : undefined;\n const noOpen = args.includes('--no-open');\n const verbose = args.includes('--verbose');\n await runOpen(cwd, { port, noOpen, verbose });\n break;\n }\n\n case '--help':\n case '-h':\n case undefined:\n printUsage();\n break;\n\n default:\n console.error(`Unknown command: ${command}`);\n printUsage();\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n console.error(error.message || error);\n process.exit(1);\n});\n"],"mappings":"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","names":["pc","resolve","framework","fs","path","fs","path","resolve","command","pc","args"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface SpoolConfig {
|
|
2
|
+
/** Dev command to start the app (e.g., "npm run dev") */
|
|
3
|
+
devCommand?: string;
|
|
4
|
+
/** Server port (default: 3142) */
|
|
5
|
+
port?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Helper to define a typed spool config
|
|
9
|
+
*/
|
|
10
|
+
declare function defineConfig(config: SpoolConfig): SpoolConfig;
|
|
11
|
+
|
|
12
|
+
export { type SpoolConfig, defineConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["export interface SpoolConfig {\n /** Dev command to start the app (e.g., \"npm run dev\") */\n devCommand?: string;\n /** Server port (default: 3142) */\n port?: number;\n}\n\n/**\n * Helper to define a typed spool config\n */\nexport function defineConfig(config: SpoolConfig): SpoolConfig {\n return config;\n}\n"],"mappings":";AAUO,SAAS,aAAa,QAAkC;AAC7D,SAAO;AACT;","names":[]}
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
interface SpoolVitePluginOptions {
|
|
4
|
+
/** Spool server port (default: 3142) */
|
|
5
|
+
port?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Vite plugin for Spool
|
|
9
|
+
* - Removes X-Frame-Options header to allow iframe embedding
|
|
10
|
+
* - Injects the inject.js script tag
|
|
11
|
+
*/
|
|
12
|
+
declare function spool(options?: SpoolVitePluginOptions): Plugin;
|
|
13
|
+
|
|
14
|
+
export { type SpoolVitePluginOptions, spool as default, spool };
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// src/vite.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
function resolveServerPort(configuredPort) {
|
|
5
|
+
try {
|
|
6
|
+
const portFile = resolve(process.cwd(), ".spool/port");
|
|
7
|
+
const content = readFileSync(portFile, "utf-8").trim();
|
|
8
|
+
const discovered = parseInt(content, 10);
|
|
9
|
+
if (!isNaN(discovered) && discovered > 0) return discovered;
|
|
10
|
+
} catch {
|
|
11
|
+
}
|
|
12
|
+
return configuredPort;
|
|
13
|
+
}
|
|
14
|
+
function reportAppUrl(appUrl, configuredPort, retriesLeft = 10) {
|
|
15
|
+
const serverPort = resolveServerPort(configuredPort);
|
|
16
|
+
fetch(`http://localhost:${serverPort}/api/app-url`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
body: JSON.stringify({ url: appUrl })
|
|
20
|
+
}).then((res) => {
|
|
21
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
22
|
+
console.log(`[spool] Reported app URL ${appUrl} to server on port ${serverPort}`);
|
|
23
|
+
}).catch(() => {
|
|
24
|
+
if (retriesLeft > 0) {
|
|
25
|
+
setTimeout(() => reportAppUrl(appUrl, configuredPort, retriesLeft - 1), 1e3);
|
|
26
|
+
} else {
|
|
27
|
+
console.warn(`[spool] Failed to report app URL to server after retries`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function spool(options = {}) {
|
|
32
|
+
const configuredPort = options.port ?? 3142;
|
|
33
|
+
return {
|
|
34
|
+
name: "spool",
|
|
35
|
+
configureServer(server) {
|
|
36
|
+
server.middlewares.use((_req, res, next) => {
|
|
37
|
+
const originalWriteHead = res.writeHead.bind(res);
|
|
38
|
+
res.writeHead = function(...args) {
|
|
39
|
+
res.removeHeader("X-Frame-Options");
|
|
40
|
+
return originalWriteHead(...args);
|
|
41
|
+
};
|
|
42
|
+
next();
|
|
43
|
+
});
|
|
44
|
+
server.httpServer?.on("listening", () => {
|
|
45
|
+
const addr = server.httpServer?.address();
|
|
46
|
+
if (addr && typeof addr === "object") {
|
|
47
|
+
reportAppUrl(`http://localhost:${addr.port}`, configuredPort);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
transformIndexHtml() {
|
|
52
|
+
const serverPort = resolveServerPort(configuredPort);
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
tag: "script",
|
|
56
|
+
attrs: {
|
|
57
|
+
src: `http://localhost:${serverPort}/inject.js`
|
|
58
|
+
},
|
|
59
|
+
injectTo: "head"
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
var vite_default = spool;
|
|
66
|
+
export {
|
|
67
|
+
vite_default as default,
|
|
68
|
+
spool
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=vite.js.map
|
package/dist/vite.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vite.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport type { Plugin } from 'vite';\n\nexport interface SpoolVitePluginOptions {\n /** Spool server port (default: 3142) */\n port?: number;\n}\n\n/**\n * Read the actual Spool server port from .spool/port (written by the server on startup).\n * Falls back to the configured port if the file doesn't exist yet.\n */\nfunction resolveServerPort(configuredPort: number): number {\n try {\n const portFile = resolve(process.cwd(), '.spool/port');\n const content = readFileSync(portFile, 'utf-8').trim();\n const discovered = parseInt(content, 10);\n if (!isNaN(discovered) && discovered > 0) return discovered;\n } catch {\n // File doesn't exist yet — server may not have started\n }\n return configuredPort;\n}\n\n/**\n * Try to POST the app URL to the Spool server, retrying if the server isn't ready yet.\n * Re-reads .spool/port on each attempt so it picks up late-starting servers.\n * Validates the response to ensure we're talking to the actual Spool server.\n */\nfunction reportAppUrl(\n appUrl: string,\n configuredPort: number,\n retriesLeft: number = 10\n): void {\n const serverPort = resolveServerPort(configuredPort);\n fetch(`http://localhost:${serverPort}/api/app-url`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ url: appUrl }),\n })\n .then((res) => {\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n console.log(`[spool] Reported app URL ${appUrl} to server on port ${serverPort}`);\n })\n .catch(() => {\n if (retriesLeft > 0) {\n setTimeout(() => reportAppUrl(appUrl, configuredPort, retriesLeft - 1), 1000);\n } else {\n console.warn(`[spool] Failed to report app URL to server after retries`);\n }\n });\n}\n\n/**\n * Vite plugin for Spool\n * - Removes X-Frame-Options header to allow iframe embedding\n * - Injects the inject.js script tag\n */\nexport function spool(options: SpoolVitePluginOptions = {}): Plugin {\n const configuredPort = options.port ?? 3142;\n\n return {\n name: 'spool',\n\n configureServer(server) {\n // Remove X-Frame-Options to allow iframe embedding\n server.middlewares.use((_req, res, next) => {\n const originalWriteHead = res.writeHead.bind(res) as typeof res.writeHead;\n res.writeHead = function (...args: Parameters<typeof originalWriteHead>) {\n res.removeHeader('X-Frame-Options');\n return originalWriteHead(...args);\n } as typeof res.writeHead;\n next();\n });\n\n // Report actual bound port to Spool server (retries if server isn't ready)\n server.httpServer?.on('listening', () => {\n const addr = server.httpServer?.address();\n if (addr && typeof addr === 'object') {\n reportAppUrl(`http://localhost:${addr.port}`, configuredPort);\n }\n });\n },\n\n transformIndexHtml() {\n const serverPort = resolveServerPort(configuredPort);\n return [\n {\n tag: 'script',\n attrs: {\n src: `http://localhost:${serverPort}/inject.js`,\n },\n injectTo: 'head',\n },\n ];\n },\n };\n}\n\nexport default spool;\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AAYxB,SAAS,kBAAkB,gBAAgC;AACzD,MAAI;AACF,UAAM,WAAW,QAAQ,QAAQ,IAAI,GAAG,aAAa;AACrD,UAAM,UAAU,aAAa,UAAU,OAAO,EAAE,KAAK;AACrD,UAAM,aAAa,SAAS,SAAS,EAAE;AACvC,QAAI,CAAC,MAAM,UAAU,KAAK,aAAa,EAAG,QAAO;AAAA,EACnD,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAOA,SAAS,aACP,QACA,gBACA,cAAsB,IAChB;AACN,QAAM,aAAa,kBAAkB,cAAc;AACnD,QAAM,oBAAoB,UAAU,gBAAgB;AAAA,IAClD,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU,EAAE,KAAK,OAAO,CAAC;AAAA,EACtC,CAAC,EACE,KAAK,CAAC,QAAQ;AACb,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,QAAQ,IAAI,MAAM,EAAE;AACjD,YAAQ,IAAI,4BAA4B,MAAM,sBAAsB,UAAU,EAAE;AAAA,EAClF,CAAC,EACA,MAAM,MAAM;AACX,QAAI,cAAc,GAAG;AACnB,iBAAW,MAAM,aAAa,QAAQ,gBAAgB,cAAc,CAAC,GAAG,GAAI;AAAA,IAC9E,OAAO;AACL,cAAQ,KAAK,0DAA0D;AAAA,IACzE;AAAA,EACF,CAAC;AACL;AAOO,SAAS,MAAM,UAAkC,CAAC,GAAW;AAClE,QAAM,iBAAiB,QAAQ,QAAQ;AAEvC,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,gBAAgB,QAAQ;AAEtB,aAAO,YAAY,IAAI,CAAC,MAAM,KAAK,SAAS;AAC1C,cAAM,oBAAoB,IAAI,UAAU,KAAK,GAAG;AAChD,YAAI,YAAY,YAAa,MAA4C;AACvE,cAAI,aAAa,iBAAiB;AAClC,iBAAO,kBAAkB,GAAG,IAAI;AAAA,QAClC;AACA,aAAK;AAAA,MACP,CAAC;AAGD,aAAO,YAAY,GAAG,aAAa,MAAM;AACvC,cAAM,OAAO,OAAO,YAAY,QAAQ;AACxC,YAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,uBAAa,oBAAoB,KAAK,IAAI,IAAI,cAAc;AAAA,QAC9D;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,qBAAqB;AACnB,YAAM,aAAa,kBAAkB,cAAc;AACnD,aAAO;AAAA,QACL;AAAA,UACE,KAAK;AAAA,UACL,OAAO;AAAA,YACL,KAAK,oBAAoB,UAAU;AAAA,UACrC;AAAA,UACA,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,eAAQ;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nicmeriano/spool",
|
|
3
|
+
"version": "0.0.0-preview.1741721",
|
|
4
|
+
"description": "Spool CLI - UI feedback tool for Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"spool": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./vite": {
|
|
17
|
+
"import": "./dist/vite.js",
|
|
18
|
+
"types": "./dist/vite.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"spool",
|
|
32
|
+
"claude",
|
|
33
|
+
"ui-feedback",
|
|
34
|
+
"cli"
|
|
35
|
+
],
|
|
36
|
+
"license": "UNLICENSED",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/nicmeriano/spool.git",
|
|
40
|
+
"directory": "packages/spool"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@nicmeriano/spool-server": "0.0.0-preview.1741721",
|
|
47
|
+
"jiti": "^2.4.0",
|
|
48
|
+
"picocolors": "^1.1.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"vite": ">=5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"vite": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^22.0.0",
|
|
60
|
+
"tsup": "^8.3.0",
|
|
61
|
+
"typescript": "^5.7.0",
|
|
62
|
+
"vite": "^6.0.0"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=18.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|