@ic-reactor/cli 0.2.0 → 0.3.1
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 +51 -193
- package/dist/index.js +77 -1080
- package/package.json +3 -3
- package/src/commands/index.ts +0 -2
- package/src/commands/init.ts +7 -7
- package/src/commands/sync.ts +20 -157
- package/src/generators/index.ts +0 -7
- package/src/generators/reactor.ts +1 -12
- package/src/index.ts +0 -20
- package/src/types.ts +1 -1
- package/src/utils/config.ts +2 -2
- package/src/commands/add.ts +0 -447
- package/src/commands/fetch.ts +0 -600
- package/src/generators/infiniteQuery.ts +0 -34
- package/src/generators/mutation.ts +0 -29
- package/src/generators/query.ts +0 -32
package/dist/index.js
CHANGED
|
@@ -12,10 +12,10 @@ import pc from "picocolors";
|
|
|
12
12
|
// src/utils/config.ts
|
|
13
13
|
import fs from "fs";
|
|
14
14
|
import path from "path";
|
|
15
|
-
var CONFIG_FILE_NAME = "reactor.
|
|
15
|
+
var CONFIG_FILE_NAME = "ic-reactor.json";
|
|
16
16
|
var DEFAULT_CONFIG = {
|
|
17
17
|
$schema: "https://raw.githubusercontent.com/B3Pay/ic-reactor/main/packages/cli/schema.json",
|
|
18
|
-
outDir: "src/canisters",
|
|
18
|
+
outDir: "src/lib/canisters",
|
|
19
19
|
canisters: {},
|
|
20
20
|
generatedHooks: {}
|
|
21
21
|
};
|
|
@@ -57,9 +57,6 @@ function ensureDir(dirPath) {
|
|
|
57
57
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
function fileExists(filePath) {
|
|
61
|
-
return fs.existsSync(filePath);
|
|
62
|
-
}
|
|
63
60
|
|
|
64
61
|
// src/commands/init.ts
|
|
65
62
|
async function initCommand(options) {
|
|
@@ -86,8 +83,8 @@ async function initCommand(options) {
|
|
|
86
83
|
} else {
|
|
87
84
|
const outDir = await p.text({
|
|
88
85
|
message: "Where should generated hooks be placed?",
|
|
89
|
-
placeholder: "src/canisters",
|
|
90
|
-
defaultValue: "src/canisters",
|
|
86
|
+
placeholder: "src/lib/canisters",
|
|
87
|
+
defaultValue: "src/lib/canisters",
|
|
91
88
|
validate: (value) => {
|
|
92
89
|
if (!value) return "Output directory is required";
|
|
93
90
|
return void 0;
|
|
@@ -120,16 +117,16 @@ async function initCommand(options) {
|
|
|
120
117
|
saveConfig(config, configPath);
|
|
121
118
|
const fullOutDir = path2.join(projectRoot, config.outDir);
|
|
122
119
|
ensureDir(fullOutDir);
|
|
123
|
-
const clientManagerPath = path2.join(projectRoot, "src/lib/
|
|
120
|
+
const clientManagerPath = path2.join(projectRoot, "src/lib/clients.ts");
|
|
124
121
|
if (!fs2.existsSync(clientManagerPath)) {
|
|
125
122
|
const createClient = await p.confirm({
|
|
126
|
-
message: "Create a sample client manager at src/lib/
|
|
123
|
+
message: "Create a sample client manager at src/lib/clients.ts?",
|
|
127
124
|
initialValue: true
|
|
128
125
|
});
|
|
129
126
|
if (!p.isCancel(createClient) && createClient) {
|
|
130
127
|
ensureDir(path2.dirname(clientManagerPath));
|
|
131
128
|
fs2.writeFileSync(clientManagerPath, getClientManagerTemplate());
|
|
132
|
-
p.log.success(`Created ${pc.green("src/lib/
|
|
129
|
+
p.log.success(`Created ${pc.green("src/lib/clients.ts")}`);
|
|
133
130
|
}
|
|
134
131
|
}
|
|
135
132
|
p.log.success(`Created ${pc.green(CONFIG_FILE_NAME)}`);
|
|
@@ -178,8 +175,8 @@ async function promptForCanister(projectRoot) {
|
|
|
178
175
|
if (p.isCancel(didFile)) return null;
|
|
179
176
|
const clientManagerPath = await p.text({
|
|
180
177
|
message: "Import path to your client manager (relative from generated hooks)",
|
|
181
|
-
placeholder: "../../
|
|
182
|
-
defaultValue: "../../
|
|
178
|
+
placeholder: "../../clients",
|
|
179
|
+
defaultValue: "../../clients"
|
|
183
180
|
});
|
|
184
181
|
if (p.isCancel(clientManagerPath)) return null;
|
|
185
182
|
const useDisplayReactor = await p.confirm({
|
|
@@ -225,79 +222,30 @@ export const clientManager = new ClientManager({
|
|
|
225
222
|
`;
|
|
226
223
|
}
|
|
227
224
|
|
|
228
|
-
// src/commands/
|
|
225
|
+
// src/commands/sync.ts
|
|
229
226
|
import * as p2 from "@clack/prompts";
|
|
230
227
|
import fs3 from "fs";
|
|
231
228
|
import path3 from "path";
|
|
232
229
|
import pc2 from "picocolors";
|
|
233
|
-
import { parseDIDFile, formatMethodForDisplay } from "@ic-reactor/codegen";
|
|
234
230
|
|
|
235
231
|
// src/generators/reactor.ts
|
|
236
232
|
import {
|
|
237
233
|
generateReactorFile as generateReactorFileFromCodegen
|
|
238
234
|
} from "@ic-reactor/codegen";
|
|
239
235
|
function generateReactorFile(options) {
|
|
240
|
-
const {
|
|
241
|
-
canisterName,
|
|
242
|
-
canisterConfig,
|
|
243
|
-
config,
|
|
244
|
-
hasDeclarations = true
|
|
245
|
-
} = options;
|
|
236
|
+
const { canisterName, canisterConfig, config } = options;
|
|
246
237
|
return generateReactorFileFromCodegen({
|
|
247
238
|
canisterName,
|
|
248
239
|
canisterConfig,
|
|
249
|
-
|
|
250
|
-
globalClientManagerPath: config.clientManagerPath,
|
|
251
|
-
// CLI doesn't currently expose advanced mode per-canister, but we default to false (simple mode)
|
|
252
|
-
// If we want to support it, we'd add 'advanced' to CanisterConfig or ReactorGeneratorOptions
|
|
253
|
-
advanced: false
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// src/generators/query.ts
|
|
258
|
-
import {
|
|
259
|
-
generateQueryHook as generateQueryHookFromCodegen
|
|
260
|
-
} from "@ic-reactor/codegen";
|
|
261
|
-
function generateQueryHook(options) {
|
|
262
|
-
const { canisterName, method, type } = options;
|
|
263
|
-
return generateQueryHookFromCodegen({
|
|
264
|
-
canisterName,
|
|
265
|
-
method,
|
|
266
|
-
type
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// src/generators/mutation.ts
|
|
271
|
-
import {
|
|
272
|
-
generateMutationHook as generateMutationHookFromCodegen
|
|
273
|
-
} from "@ic-reactor/codegen";
|
|
274
|
-
function generateMutationHook(options) {
|
|
275
|
-
const { canisterName, method } = options;
|
|
276
|
-
return generateMutationHookFromCodegen({
|
|
277
|
-
canisterName,
|
|
278
|
-
method
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// src/generators/infiniteQuery.ts
|
|
283
|
-
import {
|
|
284
|
-
generateInfiniteQueryHook as generateInfiniteQueryHookFromCodegen
|
|
285
|
-
} from "@ic-reactor/codegen";
|
|
286
|
-
function generateInfiniteQueryHook(options) {
|
|
287
|
-
const { canisterName, method, type } = options;
|
|
288
|
-
return generateInfiniteQueryHookFromCodegen({
|
|
289
|
-
canisterName,
|
|
290
|
-
method,
|
|
291
|
-
type
|
|
240
|
+
globalClientManagerPath: config.clientManagerPath
|
|
292
241
|
});
|
|
293
242
|
}
|
|
294
243
|
|
|
295
|
-
// src/commands/
|
|
296
|
-
import { getHookFileName } from "@ic-reactor/codegen";
|
|
244
|
+
// src/commands/sync.ts
|
|
297
245
|
import { generateDeclarations } from "@ic-reactor/codegen";
|
|
298
|
-
async function
|
|
246
|
+
async function syncCommand(options) {
|
|
299
247
|
console.log();
|
|
300
|
-
p2.intro(pc2.cyan("\u{
|
|
248
|
+
p2.intro(pc2.cyan("\u{1F504} Sync Canister Hooks"));
|
|
301
249
|
const configPath = findConfigFile();
|
|
302
250
|
if (!configPath) {
|
|
303
251
|
p2.log.error(
|
|
@@ -313,515 +261,94 @@ async function addCommand(options) {
|
|
|
313
261
|
const projectRoot = getProjectRoot();
|
|
314
262
|
const canisterNames = Object.keys(config.canisters);
|
|
315
263
|
if (canisterNames.length === 0) {
|
|
316
|
-
p2.log.error(
|
|
317
|
-
`No canisters configured. Add a canister to ${pc2.yellow("reactor.config.json")} first.`
|
|
318
|
-
);
|
|
319
|
-
const addNow = await p2.confirm({
|
|
320
|
-
message: "Would you like to add a canister now?",
|
|
321
|
-
initialValue: true
|
|
322
|
-
});
|
|
323
|
-
if (p2.isCancel(addNow) || !addNow) {
|
|
324
|
-
process.exit(1);
|
|
325
|
-
}
|
|
326
|
-
const canisterInfo = await promptForNewCanister(projectRoot);
|
|
327
|
-
if (!canisterInfo) {
|
|
328
|
-
p2.cancel("Cancelled.");
|
|
329
|
-
process.exit(0);
|
|
330
|
-
}
|
|
331
|
-
config.canisters[canisterInfo.name] = canisterInfo.config;
|
|
332
|
-
saveConfig(config, configPath);
|
|
333
|
-
canisterNames.push(canisterInfo.name);
|
|
334
|
-
}
|
|
335
|
-
let selectedCanister = options.canister;
|
|
336
|
-
if (!selectedCanister) {
|
|
337
|
-
if (canisterNames.length === 1) {
|
|
338
|
-
selectedCanister = canisterNames[0];
|
|
339
|
-
} else {
|
|
340
|
-
const result = await p2.select({
|
|
341
|
-
message: "Select a canister",
|
|
342
|
-
options: canisterNames.map((name) => ({
|
|
343
|
-
value: name,
|
|
344
|
-
label: name
|
|
345
|
-
}))
|
|
346
|
-
});
|
|
347
|
-
if (p2.isCancel(result)) {
|
|
348
|
-
p2.cancel("Cancelled.");
|
|
349
|
-
process.exit(0);
|
|
350
|
-
}
|
|
351
|
-
selectedCanister = result;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
const canisterConfig = config.canisters[selectedCanister];
|
|
355
|
-
if (!canisterConfig) {
|
|
356
|
-
p2.log.error(`Canister ${pc2.yellow(selectedCanister)} not found in config.`);
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
359
|
-
const didFilePath = path3.resolve(projectRoot, canisterConfig.didFile);
|
|
360
|
-
let methods;
|
|
361
|
-
try {
|
|
362
|
-
methods = parseDIDFile(didFilePath);
|
|
363
|
-
} catch (error) {
|
|
364
|
-
p2.log.error(
|
|
365
|
-
`Failed to parse DID file: ${pc2.yellow(didFilePath)}
|
|
366
|
-
${error.message}`
|
|
367
|
-
);
|
|
368
|
-
process.exit(1);
|
|
369
|
-
}
|
|
370
|
-
if (methods.length === 0) {
|
|
371
|
-
p2.log.warn(`No methods found in ${pc2.yellow(didFilePath)}`);
|
|
372
|
-
process.exit(0);
|
|
373
|
-
}
|
|
374
|
-
p2.log.info(
|
|
375
|
-
`Found ${pc2.green(methods.length.toString())} methods in ${pc2.dim(selectedCanister)}`
|
|
376
|
-
);
|
|
377
|
-
let selectedMethods;
|
|
378
|
-
if (options.all) {
|
|
379
|
-
selectedMethods = methods;
|
|
380
|
-
} else if (options.methods && options.methods.length > 0) {
|
|
381
|
-
const requestedMethods = options.methods.flatMap((m) => m.split(",")).map((m) => m.trim()).filter((m) => m.length > 0);
|
|
382
|
-
selectedMethods = methods.filter((m) => requestedMethods.includes(m.name));
|
|
383
|
-
const notFound = requestedMethods.filter(
|
|
384
|
-
(name) => !methods.some((m) => m.name === name)
|
|
385
|
-
);
|
|
386
|
-
if (notFound.length > 0) {
|
|
387
|
-
p2.log.warn(`Methods not found: ${pc2.yellow(notFound.join(", "))}`);
|
|
388
|
-
}
|
|
389
|
-
} else {
|
|
390
|
-
const alreadyGenerated = config.generatedHooks[selectedCanister] ?? [];
|
|
391
|
-
const result = await p2.multiselect({
|
|
392
|
-
message: "Select methods to add hooks for",
|
|
393
|
-
options: methods.map((method) => {
|
|
394
|
-
const isGenerated = alreadyGenerated.includes(method.name);
|
|
395
|
-
return {
|
|
396
|
-
value: method.name,
|
|
397
|
-
label: formatMethodForDisplay(method),
|
|
398
|
-
hint: isGenerated ? pc2.dim("(already generated)") : void 0
|
|
399
|
-
};
|
|
400
|
-
}),
|
|
401
|
-
required: true
|
|
402
|
-
});
|
|
403
|
-
if (p2.isCancel(result)) {
|
|
404
|
-
p2.cancel("Cancelled.");
|
|
405
|
-
process.exit(0);
|
|
406
|
-
}
|
|
407
|
-
selectedMethods = methods.filter(
|
|
408
|
-
(m) => result.includes(m.name)
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
if (selectedMethods.length === 0) {
|
|
412
|
-
p2.log.warn("No methods selected.");
|
|
413
|
-
process.exit(0);
|
|
414
|
-
}
|
|
415
|
-
const methodsWithHookTypes = [];
|
|
416
|
-
for (const method of selectedMethods) {
|
|
417
|
-
if (method.type === "query") {
|
|
418
|
-
const hookType = await p2.select({
|
|
419
|
-
message: `Hook type for ${pc2.cyan(method.name)}`,
|
|
420
|
-
options: [
|
|
421
|
-
{ value: "query", label: "Query", hint: "Standard query hook" },
|
|
422
|
-
{
|
|
423
|
-
value: "suspenseQuery",
|
|
424
|
-
label: "Suspense Query",
|
|
425
|
-
hint: "For React Suspense"
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
value: "infiniteQuery",
|
|
429
|
-
label: "Infinite Query",
|
|
430
|
-
hint: "Paginated/infinite scroll"
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
value: "suspenseInfiniteQuery",
|
|
434
|
-
label: "Suspense Infinite Query",
|
|
435
|
-
hint: "Paginated with Suspense"
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
value: "skip",
|
|
439
|
-
label: "Skip",
|
|
440
|
-
hint: "Don't generate hook for this method"
|
|
441
|
-
}
|
|
442
|
-
]
|
|
443
|
-
});
|
|
444
|
-
if (p2.isCancel(hookType)) {
|
|
445
|
-
p2.cancel("Cancelled.");
|
|
446
|
-
process.exit(0);
|
|
447
|
-
}
|
|
448
|
-
if (hookType === "skip") {
|
|
449
|
-
p2.log.info(`Skipping ${pc2.dim(method.name)}`);
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
methodsWithHookTypes.push({ method, hookType });
|
|
453
|
-
} else {
|
|
454
|
-
const hookType = await p2.select({
|
|
455
|
-
message: `Hook type for ${pc2.yellow(method.name)} (mutation)`,
|
|
456
|
-
options: [
|
|
457
|
-
{
|
|
458
|
-
value: "mutation",
|
|
459
|
-
label: "Mutation",
|
|
460
|
-
hint: "Standard mutation hook"
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
value: "skip",
|
|
464
|
-
label: "Skip",
|
|
465
|
-
hint: "Don't generate hook for this method"
|
|
466
|
-
}
|
|
467
|
-
]
|
|
468
|
-
});
|
|
469
|
-
if (p2.isCancel(hookType)) {
|
|
470
|
-
p2.cancel("Cancelled.");
|
|
471
|
-
process.exit(0);
|
|
472
|
-
}
|
|
473
|
-
if (hookType === "skip") {
|
|
474
|
-
p2.log.info(`Skipping ${pc2.dim(method.name)}`);
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
methodsWithHookTypes.push({ method, hookType: "mutation" });
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (methodsWithHookTypes.length === 0) {
|
|
481
|
-
p2.log.warn("All methods were skipped. Nothing to generate.");
|
|
482
|
-
process.exit(0);
|
|
483
|
-
}
|
|
484
|
-
const canisterOutDir = path3.join(projectRoot, config.outDir, selectedCanister);
|
|
485
|
-
const hooksOutDir = path3.join(canisterOutDir, "hooks");
|
|
486
|
-
ensureDir(hooksOutDir);
|
|
487
|
-
const spinner4 = p2.spinner();
|
|
488
|
-
spinner4.start("Generating hooks...");
|
|
489
|
-
const generatedFiles = [];
|
|
490
|
-
const reactorPath = path3.join(canisterOutDir, "reactor.ts");
|
|
491
|
-
if (!fileExists(reactorPath)) {
|
|
492
|
-
spinner4.message("Generating TypeScript declarations...");
|
|
493
|
-
const bindgenResult = await generateDeclarations({
|
|
494
|
-
didFile: didFilePath,
|
|
495
|
-
outDir: canisterOutDir,
|
|
496
|
-
canisterName: selectedCanister
|
|
497
|
-
});
|
|
498
|
-
if (bindgenResult.success) {
|
|
499
|
-
generatedFiles.push("declarations/");
|
|
500
|
-
} else {
|
|
501
|
-
p2.log.warn(`Could not generate declarations: ${bindgenResult.error}`);
|
|
502
|
-
p2.log.info(
|
|
503
|
-
`You can manually run: npx @icp-sdk/bindgen --input ${didFilePath} --output ${canisterOutDir}/declarations`
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
spinner4.message("Generating reactor...");
|
|
507
|
-
const reactorContent = generateReactorFile({
|
|
508
|
-
canisterName: selectedCanister,
|
|
509
|
-
canisterConfig,
|
|
510
|
-
config,
|
|
511
|
-
outDir: canisterOutDir,
|
|
512
|
-
hasDeclarations: bindgenResult.success
|
|
513
|
-
});
|
|
514
|
-
fs3.writeFileSync(reactorPath, reactorContent);
|
|
515
|
-
generatedFiles.push("reactor.ts");
|
|
516
|
-
}
|
|
517
|
-
for (const { method, hookType } of methodsWithHookTypes) {
|
|
518
|
-
const fileName = getHookFileName(method.name, hookType);
|
|
519
|
-
const filePath = path3.join(hooksOutDir, fileName);
|
|
520
|
-
let content;
|
|
521
|
-
switch (hookType) {
|
|
522
|
-
case "query":
|
|
523
|
-
case "suspenseQuery":
|
|
524
|
-
content = generateQueryHook({
|
|
525
|
-
canisterName: selectedCanister,
|
|
526
|
-
method,
|
|
527
|
-
config
|
|
528
|
-
});
|
|
529
|
-
break;
|
|
530
|
-
case "infiniteQuery":
|
|
531
|
-
case "suspenseInfiniteQuery":
|
|
532
|
-
content = generateInfiniteQueryHook({
|
|
533
|
-
canisterName: selectedCanister,
|
|
534
|
-
method,
|
|
535
|
-
config
|
|
536
|
-
});
|
|
537
|
-
break;
|
|
538
|
-
case "mutation":
|
|
539
|
-
content = generateMutationHook({
|
|
540
|
-
canisterName: selectedCanister,
|
|
541
|
-
method,
|
|
542
|
-
config
|
|
543
|
-
});
|
|
544
|
-
break;
|
|
545
|
-
}
|
|
546
|
-
fs3.writeFileSync(filePath, content);
|
|
547
|
-
generatedFiles.push(path3.join("hooks", fileName));
|
|
548
|
-
}
|
|
549
|
-
const indexPath = path3.join(hooksOutDir, "index.ts");
|
|
550
|
-
let existingExports = [];
|
|
551
|
-
if (fs3.existsSync(indexPath)) {
|
|
552
|
-
const content = fs3.readFileSync(indexPath, "utf-8");
|
|
553
|
-
existingExports = content.split("\n").filter((line) => line.trim().startsWith("export * from")).map((line) => line.trim());
|
|
554
|
-
}
|
|
555
|
-
const newExports = methodsWithHookTypes.map(({ method, hookType }) => {
|
|
556
|
-
const fileName = getHookFileName(method.name, hookType).replace(".ts", "");
|
|
557
|
-
return `export * from "./${fileName}"`;
|
|
558
|
-
});
|
|
559
|
-
const allExports = [.../* @__PURE__ */ new Set([...existingExports, ...newExports])];
|
|
560
|
-
const indexContent = `/**
|
|
561
|
-
* Hook barrel exports
|
|
562
|
-
*
|
|
563
|
-
* Auto-generated by @ic-reactor/cli
|
|
564
|
-
*/
|
|
565
|
-
|
|
566
|
-
${allExports.join("\n")}
|
|
567
|
-
`;
|
|
568
|
-
fs3.writeFileSync(indexPath, indexContent);
|
|
569
|
-
generatedFiles.push("hooks/index.ts");
|
|
570
|
-
const existingHooks = config.generatedHooks[selectedCanister] ?? [];
|
|
571
|
-
const newHookConfigs = methodsWithHookTypes.map(({ method, hookType }) => ({
|
|
572
|
-
name: method.name,
|
|
573
|
-
type: hookType
|
|
574
|
-
}));
|
|
575
|
-
const filteredExisting = existingHooks.filter((h) => {
|
|
576
|
-
const name = typeof h === "string" ? h : h.name;
|
|
577
|
-
return !newHookConfigs.some((n) => n.name === name);
|
|
578
|
-
});
|
|
579
|
-
config.generatedHooks[selectedCanister] = [
|
|
580
|
-
...filteredExisting,
|
|
581
|
-
...newHookConfigs
|
|
582
|
-
];
|
|
583
|
-
saveConfig(config, configPath);
|
|
584
|
-
spinner4.stop("Hooks generated!");
|
|
585
|
-
console.log();
|
|
586
|
-
p2.note(
|
|
587
|
-
generatedFiles.map((f) => pc2.green(`\u2713 ${f}`)).join("\n"),
|
|
588
|
-
`Generated in ${pc2.dim(path3.relative(projectRoot, canisterOutDir))}`
|
|
589
|
-
);
|
|
590
|
-
p2.outro(pc2.green("\u2713 Done!"));
|
|
591
|
-
}
|
|
592
|
-
async function promptForNewCanister(projectRoot) {
|
|
593
|
-
const name = await p2.text({
|
|
594
|
-
message: "Canister name",
|
|
595
|
-
placeholder: "backend",
|
|
596
|
-
validate: (value) => {
|
|
597
|
-
if (!value) return "Canister name is required";
|
|
598
|
-
return void 0;
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
|
-
if (p2.isCancel(name)) return null;
|
|
602
|
-
const didFile = await p2.text({
|
|
603
|
-
message: "Path to .did file",
|
|
604
|
-
placeholder: "./backend.did",
|
|
605
|
-
validate: (value) => {
|
|
606
|
-
if (!value) return "DID file path is required";
|
|
607
|
-
const fullPath = path3.resolve(projectRoot, value);
|
|
608
|
-
if (!fs3.existsSync(fullPath)) {
|
|
609
|
-
return `File not found: ${value}`;
|
|
610
|
-
}
|
|
611
|
-
return void 0;
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
if (p2.isCancel(didFile)) return null;
|
|
615
|
-
return {
|
|
616
|
-
name,
|
|
617
|
-
config: {
|
|
618
|
-
didFile,
|
|
619
|
-
useDisplayReactor: true
|
|
620
|
-
}
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// src/commands/sync.ts
|
|
625
|
-
import * as p3 from "@clack/prompts";
|
|
626
|
-
import fs4 from "fs";
|
|
627
|
-
import path4 from "path";
|
|
628
|
-
import pc3 from "picocolors";
|
|
629
|
-
import { parseDIDFile as parseDIDFile2 } from "@ic-reactor/codegen";
|
|
630
|
-
import { getHookFileName as getHookFileName2 } from "@ic-reactor/codegen";
|
|
631
|
-
import { generateDeclarations as generateDeclarations2 } from "@ic-reactor/codegen";
|
|
632
|
-
async function syncCommand(options) {
|
|
633
|
-
console.log();
|
|
634
|
-
p3.intro(pc3.cyan("\u{1F504} Sync Canister Hooks"));
|
|
635
|
-
const configPath = findConfigFile();
|
|
636
|
-
if (!configPath) {
|
|
637
|
-
p3.log.error(
|
|
638
|
-
`No ${pc3.yellow("reactor.config.json")} found. Run ${pc3.cyan("npx @ic-reactor/cli init")} first.`
|
|
639
|
-
);
|
|
640
|
-
process.exit(1);
|
|
641
|
-
}
|
|
642
|
-
const config = loadConfig(configPath);
|
|
643
|
-
if (!config) {
|
|
644
|
-
p3.log.error(`Failed to load config from ${pc3.yellow(configPath)}`);
|
|
645
|
-
process.exit(1);
|
|
646
|
-
}
|
|
647
|
-
const projectRoot = getProjectRoot();
|
|
648
|
-
const canisterNames = Object.keys(config.canisters);
|
|
649
|
-
if (canisterNames.length === 0) {
|
|
650
|
-
p3.log.error("No canisters configured.");
|
|
264
|
+
p2.log.error("No canisters configured.");
|
|
651
265
|
process.exit(1);
|
|
652
266
|
}
|
|
653
267
|
let canistersToSync;
|
|
654
268
|
if (options.canister) {
|
|
655
269
|
if (!config.canisters[options.canister]) {
|
|
656
|
-
|
|
657
|
-
`Canister ${
|
|
270
|
+
p2.log.error(
|
|
271
|
+
`Canister ${pc2.yellow(options.canister)} not found in config.`
|
|
658
272
|
);
|
|
659
273
|
process.exit(1);
|
|
660
274
|
}
|
|
661
275
|
canistersToSync = [options.canister];
|
|
662
276
|
} else {
|
|
663
|
-
canistersToSync = canisterNames
|
|
664
|
-
(name) => (config.generatedHooks[name]?.length ?? 0) > 0
|
|
665
|
-
);
|
|
666
|
-
if (canistersToSync.length === 0) {
|
|
667
|
-
p3.log.warn("No hooks have been generated yet. Run `add` first.");
|
|
668
|
-
process.exit(0);
|
|
669
|
-
}
|
|
277
|
+
canistersToSync = canisterNames;
|
|
670
278
|
}
|
|
671
|
-
const
|
|
672
|
-
|
|
279
|
+
const spinner2 = p2.spinner();
|
|
280
|
+
spinner2.start("Syncing hooks...");
|
|
673
281
|
let totalUpdated = 0;
|
|
674
|
-
let totalSkipped = 0;
|
|
675
282
|
const errors = [];
|
|
676
283
|
for (const canisterName of canistersToSync) {
|
|
677
284
|
const canisterConfig = config.canisters[canisterName];
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
const didFilePath = path4.resolve(projectRoot, canisterConfig.didFile);
|
|
683
|
-
let methods;
|
|
285
|
+
const canisterOutDir = path3.join(projectRoot, config.outDir, canisterName);
|
|
286
|
+
const didFilePath = path3.resolve(projectRoot, canisterConfig.didFile);
|
|
287
|
+
spinner2.message(`Regenerating declarations for ${canisterName}...`);
|
|
684
288
|
try {
|
|
685
|
-
|
|
686
|
-
} catch (error) {
|
|
687
|
-
errors.push(
|
|
688
|
-
`${canisterName}: Failed to parse DID file - ${error.message}`
|
|
689
|
-
);
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
const hooks = generatedMethods.map(
|
|
693
|
-
(h) => typeof h === "string" ? { name: h } : h
|
|
694
|
-
);
|
|
695
|
-
const currentMethodNames = methods.map((m) => m.name);
|
|
696
|
-
const removedMethods = hooks.filter((h) => !currentMethodNames.includes(h.name)).map((h) => h.name);
|
|
697
|
-
if (removedMethods.length > 0) {
|
|
698
|
-
p3.log.warn(
|
|
699
|
-
`${canisterName}: Methods removed from DID: ${pc3.yellow(removedMethods.join(", "))}`
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
const generatedNames = hooks.map((h) => h.name);
|
|
703
|
-
const newMethods = methods.filter((m) => !generatedNames.includes(m.name));
|
|
704
|
-
if (newMethods.length > 0) {
|
|
705
|
-
p3.log.info(
|
|
706
|
-
`${canisterName}: New methods available: ${pc3.cyan(newMethods.map((m) => m.name).join(", "))}`
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
const canisterOutDir = path4.join(projectRoot, config.outDir, canisterName);
|
|
710
|
-
const declarationsDir = path4.join(canisterOutDir, "declarations");
|
|
711
|
-
const declarationsExist = fs4.existsSync(declarationsDir) && fs4.readdirSync(declarationsDir).some((f) => f.endsWith(".ts") || f.endsWith(".js"));
|
|
712
|
-
if (!declarationsExist) {
|
|
713
|
-
spinner4.message(`Regenerating declarations for ${canisterName}...`);
|
|
714
|
-
const bindgenResult = await generateDeclarations2({
|
|
289
|
+
const bindgenResult = await generateDeclarations({
|
|
715
290
|
didFile: didFilePath,
|
|
716
291
|
outDir: canisterOutDir,
|
|
717
292
|
canisterName
|
|
718
293
|
});
|
|
719
|
-
if (bindgenResult.success) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
p3.log.warn(
|
|
294
|
+
if (!bindgenResult.success) {
|
|
295
|
+
errors.push(`${canisterName}: ${bindgenResult.error}`);
|
|
296
|
+
p2.log.warn(
|
|
723
297
|
`Could not regenerate declarations for ${canisterName}: ${bindgenResult.error}`
|
|
724
298
|
);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
const reactorPath = path4.join(canisterOutDir, "reactor.ts");
|
|
728
|
-
const reactorContent = generateReactorFile({
|
|
729
|
-
canisterName,
|
|
730
|
-
canisterConfig,
|
|
731
|
-
config,
|
|
732
|
-
outDir: canisterOutDir
|
|
733
|
-
});
|
|
734
|
-
fs4.writeFileSync(reactorPath, reactorContent);
|
|
735
|
-
totalUpdated++;
|
|
736
|
-
const hooksOutDir = path4.join(canisterOutDir, "hooks");
|
|
737
|
-
ensureDir(hooksOutDir);
|
|
738
|
-
for (const hookConfig of hooks) {
|
|
739
|
-
const methodName = hookConfig.name;
|
|
740
|
-
const method = methods.find((m) => m.name === methodName);
|
|
741
|
-
if (!method) {
|
|
742
|
-
totalSkipped++;
|
|
743
299
|
continue;
|
|
744
300
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
hookType = "infiniteQuery";
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
const fileName = getHookFileName2(methodName, hookType);
|
|
756
|
-
let content;
|
|
757
|
-
if (hookType.includes("Query")) {
|
|
758
|
-
content = generateQueryHook({
|
|
759
|
-
canisterName,
|
|
760
|
-
method,
|
|
761
|
-
config,
|
|
762
|
-
type: hookType
|
|
763
|
-
});
|
|
764
|
-
} else {
|
|
765
|
-
content = generateMutationHook({
|
|
766
|
-
canisterName,
|
|
767
|
-
method,
|
|
768
|
-
config
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
const filePath = path4.join(hooksOutDir, fileName);
|
|
772
|
-
if (fs4.existsSync(filePath)) {
|
|
773
|
-
const existingContent = fs4.readFileSync(filePath, "utf-8");
|
|
774
|
-
if (existingContent !== content) {
|
|
775
|
-
totalSkipped++;
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
fs4.writeFileSync(filePath, content);
|
|
301
|
+
const reactorPath = path3.join(canisterOutDir, "index.ts");
|
|
302
|
+
const reactorContent = generateReactorFile({
|
|
303
|
+
canisterName,
|
|
304
|
+
canisterConfig,
|
|
305
|
+
config
|
|
306
|
+
});
|
|
307
|
+
fs3.writeFileSync(reactorPath, reactorContent);
|
|
780
308
|
totalUpdated++;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
errors.push(`${canisterName}: ${error.message}`);
|
|
311
|
+
p2.log.error(`Failed to sync ${canisterName}: ${error.message}`);
|
|
781
312
|
}
|
|
782
313
|
}
|
|
783
|
-
|
|
314
|
+
spinner2.stop("Sync complete!");
|
|
784
315
|
if (errors.length > 0) {
|
|
785
316
|
console.log();
|
|
786
|
-
|
|
317
|
+
p2.log.error("Errors encountered:");
|
|
787
318
|
for (const error of errors) {
|
|
788
|
-
console.log(` ${
|
|
319
|
+
console.log(` ${pc2.red("\u2022")} ${error}`);
|
|
789
320
|
}
|
|
790
321
|
}
|
|
791
322
|
console.log();
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
Skipped: ${pc3.dim(totalSkipped.toString())} files (preserved customizations)`,
|
|
795
|
-
"Summary"
|
|
796
|
-
);
|
|
797
|
-
p3.outro(pc3.green("\u2713 Sync complete!"));
|
|
323
|
+
p2.note(`Updated: ${pc2.green(totalUpdated.toString())} canisters`, "Summary");
|
|
324
|
+
p2.outro(pc2.green("\u2713 Sync complete!"));
|
|
798
325
|
}
|
|
799
326
|
|
|
800
327
|
// src/commands/list.ts
|
|
801
|
-
import * as
|
|
802
|
-
import
|
|
803
|
-
import
|
|
804
|
-
import { parseDIDFile
|
|
328
|
+
import * as p3 from "@clack/prompts";
|
|
329
|
+
import path4 from "path";
|
|
330
|
+
import pc3 from "picocolors";
|
|
331
|
+
import { parseDIDFile } from "@ic-reactor/codegen";
|
|
805
332
|
async function listCommand(options) {
|
|
806
333
|
console.log();
|
|
807
|
-
|
|
334
|
+
p3.intro(pc3.cyan("\u{1F4CB} List Canister Methods"));
|
|
808
335
|
const configPath = findConfigFile();
|
|
809
336
|
if (!configPath) {
|
|
810
|
-
|
|
811
|
-
`No ${
|
|
337
|
+
p3.log.error(
|
|
338
|
+
`No ${pc3.yellow("reactor.config.json")} found. Run ${pc3.cyan("npx @ic-reactor/cli init")} first.`
|
|
812
339
|
);
|
|
813
340
|
process.exit(1);
|
|
814
341
|
}
|
|
815
342
|
const config = loadConfig(configPath);
|
|
816
343
|
if (!config) {
|
|
817
|
-
|
|
344
|
+
p3.log.error(`Failed to load config from ${pc3.yellow(configPath)}`);
|
|
818
345
|
process.exit(1);
|
|
819
346
|
}
|
|
820
347
|
const projectRoot = getProjectRoot();
|
|
821
348
|
const canisterNames = Object.keys(config.canisters);
|
|
822
349
|
if (canisterNames.length === 0) {
|
|
823
|
-
|
|
824
|
-
`No canisters configured. Add a canister to ${
|
|
350
|
+
p3.log.error(
|
|
351
|
+
`No canisters configured. Add a canister to ${pc3.yellow("reactor.config.json")} first.`
|
|
825
352
|
);
|
|
826
353
|
process.exit(1);
|
|
827
354
|
}
|
|
@@ -830,15 +357,15 @@ async function listCommand(options) {
|
|
|
830
357
|
if (canisterNames.length === 1) {
|
|
831
358
|
selectedCanister = canisterNames[0];
|
|
832
359
|
} else {
|
|
833
|
-
const result = await
|
|
360
|
+
const result = await p3.select({
|
|
834
361
|
message: "Select a canister",
|
|
835
362
|
options: canisterNames.map((name) => ({
|
|
836
363
|
value: name,
|
|
837
364
|
label: name
|
|
838
365
|
}))
|
|
839
366
|
});
|
|
840
|
-
if (
|
|
841
|
-
|
|
367
|
+
if (p3.isCancel(result)) {
|
|
368
|
+
p3.cancel("Cancelled.");
|
|
842
369
|
process.exit(0);
|
|
843
370
|
}
|
|
844
371
|
selectedCanister = result;
|
|
@@ -846,14 +373,14 @@ async function listCommand(options) {
|
|
|
846
373
|
}
|
|
847
374
|
const canisterConfig = config.canisters[selectedCanister];
|
|
848
375
|
if (!canisterConfig) {
|
|
849
|
-
|
|
376
|
+
p3.log.error(`Canister ${pc3.yellow(selectedCanister)} not found in config.`);
|
|
850
377
|
process.exit(1);
|
|
851
378
|
}
|
|
852
|
-
const didFilePath =
|
|
379
|
+
const didFilePath = path4.resolve(projectRoot, canisterConfig.didFile);
|
|
853
380
|
try {
|
|
854
|
-
const methods =
|
|
381
|
+
const methods = parseDIDFile(didFilePath);
|
|
855
382
|
if (methods.length === 0) {
|
|
856
|
-
|
|
383
|
+
p3.log.warn(`No methods found in ${pc3.yellow(didFilePath)}`);
|
|
857
384
|
process.exit(0);
|
|
858
385
|
}
|
|
859
386
|
const queries = methods.filter((m) => m.type === "query");
|
|
@@ -861,46 +388,46 @@ async function listCommand(options) {
|
|
|
861
388
|
const generatedMethods = config.generatedHooks[selectedCanister] ?? [];
|
|
862
389
|
if (queries.length > 0) {
|
|
863
390
|
console.log();
|
|
864
|
-
console.log(
|
|
391
|
+
console.log(pc3.bold(pc3.cyan(" Queries:")));
|
|
865
392
|
for (const method of queries) {
|
|
866
393
|
const isGenerated = generatedMethods.includes(method.name);
|
|
867
|
-
const status = isGenerated ?
|
|
868
|
-
const argsHint = method.hasArgs ?
|
|
394
|
+
const status = isGenerated ? pc3.green("\u2713") : pc3.dim("\u25CB");
|
|
395
|
+
const argsHint = method.hasArgs ? pc3.dim("(args)") : pc3.dim("()");
|
|
869
396
|
console.log(` ${status} ${method.name} ${argsHint}`);
|
|
870
397
|
}
|
|
871
398
|
}
|
|
872
399
|
if (mutations.length > 0) {
|
|
873
400
|
console.log();
|
|
874
|
-
console.log(
|
|
401
|
+
console.log(pc3.bold(pc3.yellow(" Mutations (Updates):")));
|
|
875
402
|
for (const method of mutations) {
|
|
876
403
|
const isGenerated = generatedMethods.includes(method.name);
|
|
877
|
-
const status = isGenerated ?
|
|
878
|
-
const argsHint = method.hasArgs ?
|
|
404
|
+
const status = isGenerated ? pc3.green("\u2713") : pc3.dim("\u25CB");
|
|
405
|
+
const argsHint = method.hasArgs ? pc3.dim("(args)") : pc3.dim("()");
|
|
879
406
|
console.log(` ${status} ${method.name} ${argsHint}`);
|
|
880
407
|
}
|
|
881
408
|
}
|
|
882
409
|
console.log();
|
|
883
410
|
const generatedCount = generatedMethods.length;
|
|
884
411
|
const totalCount = methods.length;
|
|
885
|
-
|
|
886
|
-
`Total: ${
|
|
887
|
-
Generated: ${
|
|
412
|
+
p3.note(
|
|
413
|
+
`Total: ${pc3.bold(totalCount.toString())} methods
|
|
414
|
+
Generated: ${pc3.green(generatedCount.toString())} / ${totalCount}
|
|
888
415
|
|
|
889
|
-
${
|
|
890
|
-
${
|
|
416
|
+
${pc3.green("\u2713")} = hook generated
|
|
417
|
+
${pc3.dim("\u25CB")} = not yet generated`,
|
|
891
418
|
selectedCanister
|
|
892
419
|
);
|
|
893
420
|
if (generatedCount < totalCount) {
|
|
894
421
|
console.log();
|
|
895
422
|
console.log(
|
|
896
|
-
|
|
897
|
-
` Run ${
|
|
423
|
+
pc3.dim(
|
|
424
|
+
` Run ${pc3.cyan(`npx @ic-reactor/cli add -c ${selectedCanister}`)} to add hooks`
|
|
898
425
|
)
|
|
899
426
|
);
|
|
900
427
|
}
|
|
901
428
|
} catch (error) {
|
|
902
|
-
|
|
903
|
-
`Failed to parse DID file: ${
|
|
429
|
+
p3.log.error(
|
|
430
|
+
`Failed to parse DID file: ${pc3.yellow(didFilePath)}
|
|
904
431
|
${error.message}`
|
|
905
432
|
);
|
|
906
433
|
process.exit(1);
|
|
@@ -908,543 +435,13 @@ ${error.message}`
|
|
|
908
435
|
console.log();
|
|
909
436
|
}
|
|
910
437
|
|
|
911
|
-
// src/commands/fetch.ts
|
|
912
|
-
import * as p5 from "@clack/prompts";
|
|
913
|
-
import fs5 from "fs";
|
|
914
|
-
import path6 from "path";
|
|
915
|
-
import pc5 from "picocolors";
|
|
916
|
-
import { formatMethodForDisplay as formatMethodForDisplay2 } from "@ic-reactor/codegen";
|
|
917
|
-
import { getHookFileName as getHookFileName3, toCamelCase } from "@ic-reactor/codegen";
|
|
918
|
-
|
|
919
|
-
// src/utils/network.ts
|
|
920
|
-
import { HttpAgent, Actor } from "@icp-sdk/core/agent";
|
|
921
|
-
import { Principal } from "@icp-sdk/core/principal";
|
|
922
|
-
import { IDL } from "@icp-sdk/core/candid";
|
|
923
|
-
import { extractMethods } from "@ic-reactor/codegen";
|
|
924
|
-
var IC_HOST = "https://icp-api.io";
|
|
925
|
-
var LOCAL_HOST = "http://127.0.0.1:4943";
|
|
926
|
-
async function fetchCandidFromCanister(options) {
|
|
927
|
-
const { canisterId, network = "ic", host } = options;
|
|
928
|
-
let principal;
|
|
929
|
-
try {
|
|
930
|
-
principal = Principal.fromText(canisterId);
|
|
931
|
-
} catch {
|
|
932
|
-
throw new Error(`Invalid canister ID: ${canisterId}`);
|
|
933
|
-
}
|
|
934
|
-
const agentHost = host ?? (network === "local" ? LOCAL_HOST : IC_HOST);
|
|
935
|
-
const agent = await HttpAgent.create({
|
|
936
|
-
host: agentHost,
|
|
937
|
-
// Don't verify signatures for CLI queries
|
|
938
|
-
verifyQuerySignatures: false
|
|
939
|
-
});
|
|
940
|
-
if (network === "local") {
|
|
941
|
-
try {
|
|
942
|
-
await agent.fetchRootKey();
|
|
943
|
-
} catch {
|
|
944
|
-
throw new Error(
|
|
945
|
-
`Failed to connect to local replica at ${agentHost}. Is it running?`
|
|
946
|
-
);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
const candidInterface = IDL.Service({
|
|
950
|
-
__get_candid_interface_tmp_hack: IDL.Func([], [IDL.Text], ["query"])
|
|
951
|
-
});
|
|
952
|
-
const actor = Actor.createActor(() => candidInterface, {
|
|
953
|
-
agent,
|
|
954
|
-
canisterId: principal
|
|
955
|
-
});
|
|
956
|
-
try {
|
|
957
|
-
const candidSource = await actor.__get_candid_interface_tmp_hack();
|
|
958
|
-
if (!candidSource || candidSource.trim() === "") {
|
|
959
|
-
throw new Error("Canister returned empty Candid interface");
|
|
960
|
-
}
|
|
961
|
-
const methods = extractMethods(candidSource);
|
|
962
|
-
return {
|
|
963
|
-
candidSource,
|
|
964
|
-
methods,
|
|
965
|
-
canisterId,
|
|
966
|
-
network
|
|
967
|
-
};
|
|
968
|
-
} catch (error) {
|
|
969
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
970
|
-
if (message.includes("Replica Error")) {
|
|
971
|
-
throw new Error(
|
|
972
|
-
`Canister ${canisterId} does not expose a Candid interface. The canister may not support the __get_candid_interface_tmp_hack method.`
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
if (message.includes("not found") || message.includes("404")) {
|
|
976
|
-
throw new Error(`Canister ${canisterId} not found on ${network} network.`);
|
|
977
|
-
}
|
|
978
|
-
throw new Error(`Failed to fetch Candid from canister: ${message}`);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
function isValidCanisterId(canisterId) {
|
|
982
|
-
try {
|
|
983
|
-
Principal.fromText(canisterId);
|
|
984
|
-
return true;
|
|
985
|
-
} catch {
|
|
986
|
-
return false;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
function shortenCanisterId(canisterId) {
|
|
990
|
-
if (canisterId.length <= 15) return canisterId;
|
|
991
|
-
return `${canisterId.slice(0, 5)}...${canisterId.slice(-5)}`;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// src/commands/fetch.ts
|
|
995
|
-
import { generateDeclarations as generateDeclarations3 } from "@ic-reactor/codegen";
|
|
996
|
-
async function fetchCommand(options) {
|
|
997
|
-
console.log();
|
|
998
|
-
p5.intro(pc5.cyan("\u{1F310} Fetch from Live Canister"));
|
|
999
|
-
const projectRoot = getProjectRoot();
|
|
1000
|
-
let canisterId = options.canisterId;
|
|
1001
|
-
if (!canisterId) {
|
|
1002
|
-
const input = await p5.text({
|
|
1003
|
-
message: "Enter canister ID",
|
|
1004
|
-
placeholder: "ryjl3-tyaaa-aaaaa-aaaba-cai",
|
|
1005
|
-
validate: (value) => {
|
|
1006
|
-
if (!value) return "Canister ID is required";
|
|
1007
|
-
if (!isValidCanisterId(value)) {
|
|
1008
|
-
return "Invalid canister ID format";
|
|
1009
|
-
}
|
|
1010
|
-
return void 0;
|
|
1011
|
-
}
|
|
1012
|
-
});
|
|
1013
|
-
if (p5.isCancel(input)) {
|
|
1014
|
-
p5.cancel("Cancelled.");
|
|
1015
|
-
process.exit(0);
|
|
1016
|
-
}
|
|
1017
|
-
canisterId = input;
|
|
1018
|
-
}
|
|
1019
|
-
if (!isValidCanisterId(canisterId)) {
|
|
1020
|
-
p5.log.error(`Invalid canister ID: ${pc5.yellow(canisterId)}`);
|
|
1021
|
-
process.exit(1);
|
|
1022
|
-
}
|
|
1023
|
-
let network = options.network;
|
|
1024
|
-
if (!network) {
|
|
1025
|
-
const result = await p5.select({
|
|
1026
|
-
message: "Select network",
|
|
1027
|
-
options: [
|
|
1028
|
-
{ value: "ic", label: "IC Mainnet", hint: "icp-api.io" },
|
|
1029
|
-
{ value: "local", label: "Local Replica", hint: "localhost:4943" }
|
|
1030
|
-
]
|
|
1031
|
-
});
|
|
1032
|
-
if (p5.isCancel(result)) {
|
|
1033
|
-
p5.cancel("Cancelled.");
|
|
1034
|
-
process.exit(0);
|
|
1035
|
-
}
|
|
1036
|
-
network = result;
|
|
1037
|
-
}
|
|
1038
|
-
const spinner4 = p5.spinner();
|
|
1039
|
-
spinner4.start(`Fetching Candid from ${shortenCanisterId(canisterId)}...`);
|
|
1040
|
-
let candidResult;
|
|
1041
|
-
try {
|
|
1042
|
-
candidResult = await fetchCandidFromCanister({
|
|
1043
|
-
canisterId,
|
|
1044
|
-
network
|
|
1045
|
-
});
|
|
1046
|
-
spinner4.stop(
|
|
1047
|
-
`Found ${pc5.green(candidResult.methods.length.toString())} methods`
|
|
1048
|
-
);
|
|
1049
|
-
} catch (error) {
|
|
1050
|
-
spinner4.stop("Failed to fetch Candid");
|
|
1051
|
-
p5.log.error(error.message);
|
|
1052
|
-
process.exit(1);
|
|
1053
|
-
}
|
|
1054
|
-
const { methods, candidSource } = candidResult;
|
|
1055
|
-
if (methods.length === 0) {
|
|
1056
|
-
p5.log.warn("No methods found in canister interface");
|
|
1057
|
-
process.exit(0);
|
|
1058
|
-
}
|
|
1059
|
-
let canisterName = options.name;
|
|
1060
|
-
if (!canisterName) {
|
|
1061
|
-
const input = await p5.text({
|
|
1062
|
-
message: "Name for this canister (used in generated code)",
|
|
1063
|
-
placeholder: toCamelCase(canisterId.split("-")[0]),
|
|
1064
|
-
defaultValue: toCamelCase(canisterId.split("-")[0]),
|
|
1065
|
-
validate: (value) => {
|
|
1066
|
-
if (!value) return "Name is required";
|
|
1067
|
-
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
|
|
1068
|
-
return "Name must start with a letter and contain only letters, numbers, hyphens, and underscores";
|
|
1069
|
-
}
|
|
1070
|
-
return void 0;
|
|
1071
|
-
}
|
|
1072
|
-
});
|
|
1073
|
-
if (p5.isCancel(input)) {
|
|
1074
|
-
p5.cancel("Cancelled.");
|
|
1075
|
-
process.exit(0);
|
|
1076
|
-
}
|
|
1077
|
-
canisterName = input;
|
|
1078
|
-
}
|
|
1079
|
-
let selectedMethods;
|
|
1080
|
-
if (options.all) {
|
|
1081
|
-
selectedMethods = methods;
|
|
1082
|
-
} else if (options.methods && options.methods.length > 0) {
|
|
1083
|
-
const requestedMethods = options.methods.flatMap((m) => m.split(",")).map((m) => m.trim()).filter((m) => m.length > 0);
|
|
1084
|
-
selectedMethods = methods.filter((m) => requestedMethods.includes(m.name));
|
|
1085
|
-
const notFound = requestedMethods.filter(
|
|
1086
|
-
(name) => !methods.some((m) => m.name === name)
|
|
1087
|
-
);
|
|
1088
|
-
if (notFound.length > 0) {
|
|
1089
|
-
p5.log.warn(`Methods not found: ${pc5.yellow(notFound.join(", "))}`);
|
|
1090
|
-
}
|
|
1091
|
-
} else {
|
|
1092
|
-
const result = await p5.multiselect({
|
|
1093
|
-
message: "Select methods to generate hooks for",
|
|
1094
|
-
options: methods.map((method) => ({
|
|
1095
|
-
value: method.name,
|
|
1096
|
-
label: formatMethodForDisplay2(method)
|
|
1097
|
-
})),
|
|
1098
|
-
required: true
|
|
1099
|
-
});
|
|
1100
|
-
if (p5.isCancel(result)) {
|
|
1101
|
-
p5.cancel("Cancelled.");
|
|
1102
|
-
process.exit(0);
|
|
1103
|
-
}
|
|
1104
|
-
selectedMethods = methods.filter(
|
|
1105
|
-
(m) => result.includes(m.name)
|
|
1106
|
-
);
|
|
1107
|
-
}
|
|
1108
|
-
if (selectedMethods.length === 0) {
|
|
1109
|
-
p5.log.warn("No methods selected.");
|
|
1110
|
-
process.exit(0);
|
|
1111
|
-
}
|
|
1112
|
-
const methodsWithHookTypes = [];
|
|
1113
|
-
for (const method of selectedMethods) {
|
|
1114
|
-
if (method.type === "query") {
|
|
1115
|
-
const hookType = await p5.select({
|
|
1116
|
-
message: `Hook type for ${pc5.cyan(method.name)}`,
|
|
1117
|
-
options: [
|
|
1118
|
-
{
|
|
1119
|
-
value: "skip",
|
|
1120
|
-
label: "Skip",
|
|
1121
|
-
hint: "Don't generate hook for this method"
|
|
1122
|
-
},
|
|
1123
|
-
{ value: "query", label: "Query", hint: "Standard query hook" },
|
|
1124
|
-
{
|
|
1125
|
-
value: "suspenseQuery",
|
|
1126
|
-
label: "Suspense Query",
|
|
1127
|
-
hint: "For React Suspense"
|
|
1128
|
-
},
|
|
1129
|
-
{
|
|
1130
|
-
value: "infiniteQuery",
|
|
1131
|
-
label: "Infinite Query",
|
|
1132
|
-
hint: "Paginated/infinite scroll"
|
|
1133
|
-
},
|
|
1134
|
-
{
|
|
1135
|
-
value: "suspenseInfiniteQuery",
|
|
1136
|
-
label: "Suspense Infinite Query",
|
|
1137
|
-
hint: "Paginated with Suspense"
|
|
1138
|
-
}
|
|
1139
|
-
]
|
|
1140
|
-
});
|
|
1141
|
-
if (p5.isCancel(hookType)) {
|
|
1142
|
-
p5.cancel("Cancelled.");
|
|
1143
|
-
process.exit(0);
|
|
1144
|
-
}
|
|
1145
|
-
if (hookType === "skip") {
|
|
1146
|
-
p5.log.info(`Skipping ${pc5.dim(method.name)}`);
|
|
1147
|
-
continue;
|
|
1148
|
-
}
|
|
1149
|
-
methodsWithHookTypes.push({ method, hookType });
|
|
1150
|
-
} else {
|
|
1151
|
-
const hookType = await p5.select({
|
|
1152
|
-
message: `Hook type for ${pc5.yellow(method.name)} (mutation)`,
|
|
1153
|
-
options: [
|
|
1154
|
-
{
|
|
1155
|
-
value: "mutation",
|
|
1156
|
-
label: "Mutation",
|
|
1157
|
-
hint: "Standard mutation hook"
|
|
1158
|
-
},
|
|
1159
|
-
{
|
|
1160
|
-
value: "skip",
|
|
1161
|
-
label: "Skip",
|
|
1162
|
-
hint: "Don't generate hook for this method"
|
|
1163
|
-
}
|
|
1164
|
-
]
|
|
1165
|
-
});
|
|
1166
|
-
if (p5.isCancel(hookType)) {
|
|
1167
|
-
p5.cancel("Cancelled.");
|
|
1168
|
-
process.exit(0);
|
|
1169
|
-
}
|
|
1170
|
-
if (hookType === "skip") {
|
|
1171
|
-
p5.log.info(`Skipping ${pc5.dim(method.name)}`);
|
|
1172
|
-
continue;
|
|
1173
|
-
}
|
|
1174
|
-
methodsWithHookTypes.push({ method, hookType: "mutation" });
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
if (methodsWithHookTypes.length === 0) {
|
|
1178
|
-
p5.log.warn("All methods were skipped. Nothing to generate.");
|
|
1179
|
-
process.exit(0);
|
|
1180
|
-
}
|
|
1181
|
-
let configPath = findConfigFile();
|
|
1182
|
-
let config;
|
|
1183
|
-
if (!configPath) {
|
|
1184
|
-
configPath = path6.join(projectRoot, CONFIG_FILE_NAME);
|
|
1185
|
-
config = { ...DEFAULT_CONFIG };
|
|
1186
|
-
p5.log.info(`Creating ${pc5.yellow(CONFIG_FILE_NAME)}`);
|
|
1187
|
-
} else {
|
|
1188
|
-
config = loadConfig(configPath) ?? { ...DEFAULT_CONFIG };
|
|
1189
|
-
}
|
|
1190
|
-
const canisterConfig = {
|
|
1191
|
-
didFile: `./candid/${canisterName}.did`,
|
|
1192
|
-
useDisplayReactor: true,
|
|
1193
|
-
canisterId
|
|
1194
|
-
};
|
|
1195
|
-
config.canisters[canisterName] = canisterConfig;
|
|
1196
|
-
const canisterOutDir = path6.join(projectRoot, config.outDir, canisterName);
|
|
1197
|
-
const hooksOutDir = path6.join(canisterOutDir, "hooks");
|
|
1198
|
-
const candidDir = path6.join(projectRoot, "candid");
|
|
1199
|
-
ensureDir(hooksOutDir);
|
|
1200
|
-
ensureDir(candidDir);
|
|
1201
|
-
const genSpinner = p5.spinner();
|
|
1202
|
-
genSpinner.start("Generating hooks...");
|
|
1203
|
-
const generatedFiles = [];
|
|
1204
|
-
const candidPath = path6.join(candidDir, `${canisterName}.did`);
|
|
1205
|
-
fs5.writeFileSync(candidPath, candidSource);
|
|
1206
|
-
generatedFiles.push(`candid/${canisterName}.did`);
|
|
1207
|
-
genSpinner.message("Generating TypeScript declarations...");
|
|
1208
|
-
const bindgenResult = await generateDeclarations3({
|
|
1209
|
-
didFile: candidPath,
|
|
1210
|
-
outDir: canisterOutDir,
|
|
1211
|
-
canisterName
|
|
1212
|
-
});
|
|
1213
|
-
if (bindgenResult.success) {
|
|
1214
|
-
generatedFiles.push("declarations/");
|
|
1215
|
-
} else {
|
|
1216
|
-
p5.log.warn(`Could not generate declarations: ${bindgenResult.error}`);
|
|
1217
|
-
p5.log.info(
|
|
1218
|
-
`You can manually run: npx @icp-sdk/bindgen --input ${candidPath} --output ${canisterOutDir}/declarations`
|
|
1219
|
-
);
|
|
1220
|
-
}
|
|
1221
|
-
genSpinner.message("Generating reactor...");
|
|
1222
|
-
const reactorPath = path6.join(canisterOutDir, "reactor.ts");
|
|
1223
|
-
const reactorContent = generateReactorFileForFetch({
|
|
1224
|
-
canisterName,
|
|
1225
|
-
canisterConfig,
|
|
1226
|
-
config,
|
|
1227
|
-
canisterId,
|
|
1228
|
-
hasDeclarations: bindgenResult.success
|
|
1229
|
-
});
|
|
1230
|
-
fs5.writeFileSync(reactorPath, reactorContent);
|
|
1231
|
-
generatedFiles.push("reactor.ts");
|
|
1232
|
-
for (const { method, hookType } of methodsWithHookTypes) {
|
|
1233
|
-
const fileName = getHookFileName3(method.name, hookType);
|
|
1234
|
-
const filePath = path6.join(hooksOutDir, fileName);
|
|
1235
|
-
let content;
|
|
1236
|
-
switch (hookType) {
|
|
1237
|
-
case "query":
|
|
1238
|
-
case "suspenseQuery":
|
|
1239
|
-
content = generateQueryHook({
|
|
1240
|
-
canisterName,
|
|
1241
|
-
method,
|
|
1242
|
-
config
|
|
1243
|
-
});
|
|
1244
|
-
break;
|
|
1245
|
-
case "infiniteQuery":
|
|
1246
|
-
case "suspenseInfiniteQuery":
|
|
1247
|
-
content = generateInfiniteQueryHook({
|
|
1248
|
-
canisterName,
|
|
1249
|
-
method,
|
|
1250
|
-
config
|
|
1251
|
-
});
|
|
1252
|
-
break;
|
|
1253
|
-
case "mutation":
|
|
1254
|
-
content = generateMutationHook({
|
|
1255
|
-
canisterName,
|
|
1256
|
-
method,
|
|
1257
|
-
config
|
|
1258
|
-
});
|
|
1259
|
-
break;
|
|
1260
|
-
}
|
|
1261
|
-
fs5.writeFileSync(filePath, content);
|
|
1262
|
-
generatedFiles.push(path6.join("hooks", fileName));
|
|
1263
|
-
}
|
|
1264
|
-
const indexPath = path6.join(hooksOutDir, "index.ts");
|
|
1265
|
-
const indexContent = generateIndexFile(methodsWithHookTypes);
|
|
1266
|
-
fs5.writeFileSync(indexPath, indexContent);
|
|
1267
|
-
generatedFiles.push("hooks/index.ts");
|
|
1268
|
-
config.generatedHooks[canisterName] = [
|
|
1269
|
-
.../* @__PURE__ */ new Set([
|
|
1270
|
-
...config.generatedHooks[canisterName] ?? [],
|
|
1271
|
-
...selectedMethods.map((m) => m.name)
|
|
1272
|
-
])
|
|
1273
|
-
];
|
|
1274
|
-
saveConfig(config, configPath);
|
|
1275
|
-
genSpinner.stop("Hooks generated!");
|
|
1276
|
-
console.log();
|
|
1277
|
-
p5.note(
|
|
1278
|
-
generatedFiles.map((f) => pc5.green(`\u2713 ${f}`)).join("\n"),
|
|
1279
|
-
`Generated in ${pc5.dim(path6.relative(projectRoot, canisterOutDir))}`
|
|
1280
|
-
);
|
|
1281
|
-
console.log();
|
|
1282
|
-
p5.note(
|
|
1283
|
-
`Canister ID: ${pc5.cyan(canisterId)}
|
|
1284
|
-
Network: ${pc5.yellow(network)}
|
|
1285
|
-
Name: ${pc5.green(canisterName)}
|
|
1286
|
-
Methods: ${pc5.dim(selectedMethods.map((m) => m.name).join(", "))}`,
|
|
1287
|
-
"Canister Info"
|
|
1288
|
-
);
|
|
1289
|
-
p5.outro(pc5.green("\u2713 Done!"));
|
|
1290
|
-
}
|
|
1291
|
-
function generateReactorFileForFetch(options) {
|
|
1292
|
-
const { canisterName, canisterConfig, config, canisterId, hasDeclarations } = options;
|
|
1293
|
-
const pascalName = canisterName.charAt(0).toUpperCase() + canisterName.slice(1);
|
|
1294
|
-
const reactorName = `${canisterName}Reactor`;
|
|
1295
|
-
const serviceName = `${pascalName}Service`;
|
|
1296
|
-
const reactorType = canisterConfig.useDisplayReactor !== false ? "DisplayReactor" : "Reactor";
|
|
1297
|
-
const clientManagerPath = canisterConfig.clientManagerPath ?? config.clientManagerPath ?? "../../lib/client";
|
|
1298
|
-
const didFileName = path6.basename(canisterConfig.didFile);
|
|
1299
|
-
if (hasDeclarations) {
|
|
1300
|
-
return `/**
|
|
1301
|
-
* ${pascalName} Reactor
|
|
1302
|
-
*
|
|
1303
|
-
* Auto-generated by @ic-reactor/cli
|
|
1304
|
-
* Fetched from canister: ${canisterId}
|
|
1305
|
-
*
|
|
1306
|
-
* You can customize this file to add global configuration.
|
|
1307
|
-
*/
|
|
1308
|
-
|
|
1309
|
-
import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
|
|
1310
|
-
import { clientManager } from "${clientManagerPath}"
|
|
1311
|
-
|
|
1312
|
-
// Import generated declarations
|
|
1313
|
-
import { idlFactory, type _SERVICE as ${serviceName} } from "./declarations/${didFileName}"
|
|
1314
|
-
|
|
1315
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1316
|
-
// REACTOR INSTANCE
|
|
1317
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1318
|
-
|
|
1319
|
-
/**
|
|
1320
|
-
* ${pascalName} Reactor
|
|
1321
|
-
*
|
|
1322
|
-
* Canister ID: ${canisterId}
|
|
1323
|
-
*/
|
|
1324
|
-
export const ${reactorName} = new ${reactorType}<${serviceName}>({
|
|
1325
|
-
clientManager,
|
|
1326
|
-
idlFactory,
|
|
1327
|
-
canisterId: "${canisterId}",
|
|
1328
|
-
name: "${canisterName}",
|
|
1329
|
-
})
|
|
1330
|
-
|
|
1331
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1332
|
-
// ACTOR HOOKS
|
|
1333
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1334
|
-
|
|
1335
|
-
/**
|
|
1336
|
-
* Actor hooks for ${canisterName} - use these directly or import method-specific hooks.
|
|
1337
|
-
*/
|
|
1338
|
-
export const {
|
|
1339
|
-
useActorQuery,
|
|
1340
|
-
useActorMutation,
|
|
1341
|
-
useActorSuspenseQuery,
|
|
1342
|
-
useActorInfiniteQuery,
|
|
1343
|
-
useActorSuspenseInfiniteQuery,
|
|
1344
|
-
useActorMethod,
|
|
1345
|
-
} = createActorHooks(${reactorName})
|
|
1346
|
-
|
|
1347
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1348
|
-
// RE-EXPORTS
|
|
1349
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1350
|
-
|
|
1351
|
-
export { idlFactory }
|
|
1352
|
-
export type { ${serviceName} }
|
|
1353
|
-
`;
|
|
1354
|
-
}
|
|
1355
|
-
return `/**
|
|
1356
|
-
* ${pascalName} Reactor
|
|
1357
|
-
*
|
|
1358
|
-
* Auto-generated by @ic-reactor/cli
|
|
1359
|
-
* Fetched from canister: ${canisterId}
|
|
1360
|
-
*
|
|
1361
|
-
* You can customize this file to add global configuration.
|
|
1362
|
-
*/
|
|
1363
|
-
|
|
1364
|
-
import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
|
|
1365
|
-
import { clientManager } from "${clientManagerPath}"
|
|
1366
|
-
|
|
1367
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1368
|
-
// DECLARATIONS
|
|
1369
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1370
|
-
|
|
1371
|
-
// TODO: Generate proper types by running:
|
|
1372
|
-
// npx @icp-sdk/bindgen --input ./candid/${canisterName}.did --output ./${canisterName}/declarations
|
|
1373
|
-
|
|
1374
|
-
// For now, import just the IDL factory (you may need to create this manually)
|
|
1375
|
-
// import { idlFactory, type _SERVICE as ${serviceName} } from "./declarations/${didFileName}"
|
|
1376
|
-
|
|
1377
|
-
// Fallback generic type - replace with generated types
|
|
1378
|
-
type ${serviceName} = Record<string, (...args: unknown[]) => Promise<unknown>>
|
|
1379
|
-
|
|
1380
|
-
// You'll need to define idlFactory here or import from declarations
|
|
1381
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1382
|
-
const idlFactory = ({ IDL }: { IDL: any }) => IDL.Service({})
|
|
1383
|
-
|
|
1384
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1385
|
-
// REACTOR INSTANCE
|
|
1386
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1387
|
-
|
|
1388
|
-
/**
|
|
1389
|
-
* ${pascalName} Reactor
|
|
1390
|
-
*
|
|
1391
|
-
* Canister ID: ${canisterId}
|
|
1392
|
-
*/
|
|
1393
|
-
export const ${reactorName} = new ${reactorType}<${serviceName}>({
|
|
1394
|
-
clientManager,
|
|
1395
|
-
idlFactory,
|
|
1396
|
-
canisterId: "${canisterId}",
|
|
1397
|
-
name: "${canisterName}",
|
|
1398
|
-
})
|
|
1399
|
-
|
|
1400
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1401
|
-
// ACTOR HOOKS
|
|
1402
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1403
|
-
|
|
1404
|
-
/**
|
|
1405
|
-
* Actor hooks for ${canisterName} - use these directly or import method-specific hooks.
|
|
1406
|
-
*/
|
|
1407
|
-
export const {
|
|
1408
|
-
useActorQuery,
|
|
1409
|
-
useActorMutation,
|
|
1410
|
-
useActorSuspenseQuery,
|
|
1411
|
-
useActorInfiniteQuery,
|
|
1412
|
-
useActorSuspenseInfiniteQuery,
|
|
1413
|
-
useActorMethod,
|
|
1414
|
-
} = createActorHooks(${reactorName})
|
|
1415
|
-
|
|
1416
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1417
|
-
// RE-EXPORTS
|
|
1418
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1419
|
-
|
|
1420
|
-
export { idlFactory }
|
|
1421
|
-
export type { ${serviceName} }
|
|
1422
|
-
`;
|
|
1423
|
-
}
|
|
1424
|
-
function generateIndexFile(methods) {
|
|
1425
|
-
const exports = methods.map(({ method, hookType }) => {
|
|
1426
|
-
const fileName = getHookFileName3(method.name, hookType).replace(".ts", "");
|
|
1427
|
-
return `export * from "./${fileName}"`;
|
|
1428
|
-
});
|
|
1429
|
-
return `/**
|
|
1430
|
-
* Hook barrel exports
|
|
1431
|
-
*
|
|
1432
|
-
* Auto-generated by @ic-reactor/cli
|
|
1433
|
-
*/
|
|
1434
|
-
|
|
1435
|
-
${exports.join("\n")}
|
|
1436
|
-
`;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
438
|
// src/index.ts
|
|
1440
|
-
import
|
|
439
|
+
import pc4 from "picocolors";
|
|
1441
440
|
var program = new Command();
|
|
1442
441
|
program.name("ic-reactor").description(
|
|
1443
|
-
|
|
442
|
+
pc4.cyan("\u{1F527} Generate shadcn-style React hooks for ICP canisters")
|
|
1444
443
|
).version("3.0.0");
|
|
1445
444
|
program.command("init").description("Initialize ic-reactor configuration in your project").option("-y, --yes", "Skip prompts and use defaults").option("-o, --out-dir <path>", "Output directory for generated hooks").action(initCommand);
|
|
1446
|
-
program.command("add").description("Add hooks for canister methods (from local .did file)").option("-c, --canister <name>", "Canister name to add hooks for").option("-m, --methods <methods...>", "Method names to generate hooks for").option("-a, --all", "Add hooks for all methods").action(addCommand);
|
|
1447
|
-
program.command("fetch").description("Fetch Candid from a live canister and generate hooks").option("-i, --canister-id <id>", "Canister ID to fetch from").option("-n, --network <network>", "Network: 'ic' or 'local'", "ic").option("--name <name>", "Name for the canister in generated code").option("-m, --methods <methods...>", "Method names to generate hooks for").option("-a, --all", "Add hooks for all methods").action(fetchCommand);
|
|
1448
445
|
program.command("sync").description("Sync hooks with .did file changes").option("-c, --canister <name>", "Canister to sync").action(syncCommand);
|
|
1449
446
|
program.command("list").description("List available methods from a canister").option("-c, --canister <name>", "Canister to list methods from").action(listCommand);
|
|
1450
447
|
program.parse();
|