@ic-reactor/cli 0.4.1 → 0.5.0
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 +42 -39
- package/dist/index.js +96 -281
- package/package.json +2 -2
- package/schema.json +21 -46
- package/src/commands/generate.ts +114 -0
- package/src/commands/init.ts +57 -124
- package/src/index.ts +12 -19
- package/src/types.ts +18 -17
- package/src/utils/config.ts +6 -29
- package/src/commands/list.ts +0 -144
- package/src/commands/sync.ts +0 -122
- package/src/generators/index.ts +0 -1
- package/src/generators/reactor.ts +0 -30
package/schema.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"title": "IC Reactor
|
|
4
|
-
"description": "Configuration file for @ic-reactor/cli",
|
|
3
|
+
"title": "IC Reactor Configuration",
|
|
4
|
+
"description": "Configuration file for @ic-reactor/cli and @ic-reactor/vite-plugin",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"properties": {
|
|
7
7
|
"$schema": {
|
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
},
|
|
11
11
|
"outDir": {
|
|
12
12
|
"type": "string",
|
|
13
|
-
"description": "
|
|
14
|
-
"default": "src/
|
|
13
|
+
"description": "Default output directory for generated files (relative to project root)",
|
|
14
|
+
"default": "src/declarations"
|
|
15
|
+
},
|
|
16
|
+
"clientManagerPath": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Default import path for the client manager (relative from generated files)",
|
|
19
|
+
"default": "../../clients"
|
|
15
20
|
},
|
|
16
21
|
"canisters": {
|
|
17
22
|
"type": "object",
|
|
@@ -19,58 +24,28 @@
|
|
|
19
24
|
"additionalProperties": {
|
|
20
25
|
"type": "object",
|
|
21
26
|
"properties": {
|
|
27
|
+
"name": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Canister name (required)"
|
|
30
|
+
},
|
|
22
31
|
"didFile": {
|
|
23
32
|
"type": "string",
|
|
24
|
-
"description": "Path to the .did file (
|
|
33
|
+
"description": "Path to the .did file (required)"
|
|
25
34
|
},
|
|
26
|
-
"
|
|
35
|
+
"outDir": {
|
|
27
36
|
"type": "string",
|
|
28
|
-
"description": "
|
|
29
|
-
"default": "../../lib/client"
|
|
37
|
+
"description": "Override output directory for this canister"
|
|
30
38
|
},
|
|
31
|
-
"
|
|
32
|
-
"type": "
|
|
33
|
-
"description": "
|
|
34
|
-
"default": true
|
|
39
|
+
"clientManagerPath": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Override client manager import path for this canister"
|
|
35
42
|
},
|
|
36
43
|
"canisterId": {
|
|
37
44
|
"type": "string",
|
|
38
|
-
"description": "Optional fixed canister ID
|
|
45
|
+
"description": "Optional fixed canister ID"
|
|
39
46
|
}
|
|
40
47
|
},
|
|
41
|
-
"required": ["didFile"]
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"generatedHooks": {
|
|
45
|
-
"type": "object",
|
|
46
|
-
"description": "Tracks which methods have generated hooks (managed by CLI)",
|
|
47
|
-
"additionalProperties": {
|
|
48
|
-
"items": {
|
|
49
|
-
"anyOf": [
|
|
50
|
-
{
|
|
51
|
-
"type": "string"
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"type": "object",
|
|
55
|
-
"properties": {
|
|
56
|
-
"name": {
|
|
57
|
-
"type": "string"
|
|
58
|
-
},
|
|
59
|
-
"type": {
|
|
60
|
-
"type": "string",
|
|
61
|
-
"enum": [
|
|
62
|
-
"query",
|
|
63
|
-
"mutation",
|
|
64
|
-
"suspenseQuery",
|
|
65
|
-
"infiniteQuery",
|
|
66
|
-
"suspenseInfiniteQuery"
|
|
67
|
-
]
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"required": ["name"]
|
|
71
|
-
}
|
|
72
|
-
]
|
|
73
|
-
}
|
|
48
|
+
"required": ["name", "didFile"]
|
|
74
49
|
}
|
|
75
50
|
}
|
|
76
51
|
},
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Command
|
|
3
|
+
*
|
|
4
|
+
* Runs the codegen pipeline for configured canisters.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as p from "@clack/prompts"
|
|
8
|
+
import pc from "picocolors"
|
|
9
|
+
import { loadConfig, findConfigFile, getProjectRoot } from "../utils/config.js"
|
|
10
|
+
import { runCanisterPipeline } from "@ic-reactor/codegen"
|
|
11
|
+
import type { GenerateOptions } from "../types.js"
|
|
12
|
+
|
|
13
|
+
export async function generateCommand(options: GenerateOptions) {
|
|
14
|
+
console.log()
|
|
15
|
+
p.intro(pc.cyan("🔄 Generate Hooks"))
|
|
16
|
+
|
|
17
|
+
// Load config
|
|
18
|
+
const configPath = findConfigFile()
|
|
19
|
+
if (!configPath) {
|
|
20
|
+
p.log.error(
|
|
21
|
+
`No ${pc.yellow("ic-reactor.json")} found. Run ${pc.cyan("npx ic-reactor init")} first.`
|
|
22
|
+
)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = loadConfig(configPath)
|
|
27
|
+
if (!config) {
|
|
28
|
+
p.log.error(`Failed to load config from ${pc.yellow(configPath)}`)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const projectRoot = getProjectRoot()
|
|
33
|
+
const canisterNames = Object.keys(config.canisters)
|
|
34
|
+
|
|
35
|
+
if (canisterNames.length === 0) {
|
|
36
|
+
p.log.error("No canisters configured.")
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Determine which canisters to process
|
|
41
|
+
let canistersToProcess: string[] = []
|
|
42
|
+
|
|
43
|
+
if (options.canister) {
|
|
44
|
+
if (!config.canisters[options.canister]) {
|
|
45
|
+
p.log.error(
|
|
46
|
+
`Canister ${pc.yellow(options.canister)} not found in config.`
|
|
47
|
+
)
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
canistersToProcess = [options.canister]
|
|
51
|
+
} else {
|
|
52
|
+
canistersToProcess = canisterNames
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const spinner = p.spinner()
|
|
56
|
+
spinner.start(
|
|
57
|
+
`Generating hooks for ${canistersToProcess.length} canisters...`
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
let successCount = 0
|
|
61
|
+
let errorCount = 0
|
|
62
|
+
const errorMessages: string[] = []
|
|
63
|
+
|
|
64
|
+
// Run pipeline for each canister
|
|
65
|
+
for (const name of canistersToProcess) {
|
|
66
|
+
const canisterConfig = config.canisters[name]
|
|
67
|
+
|
|
68
|
+
spinner.message(`Processing ${pc.cyan(name)}...`)
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const result = await runCanisterPipeline({
|
|
72
|
+
canisterConfig,
|
|
73
|
+
projectRoot,
|
|
74
|
+
globalConfig: config,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (result.success) {
|
|
78
|
+
successCount++
|
|
79
|
+
} else {
|
|
80
|
+
errorCount++
|
|
81
|
+
errorMessages.push(`${name}: ${result.error}`)
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
errorCount++
|
|
85
|
+
errorMessages.push(
|
|
86
|
+
`${name}: ${err instanceof Error ? err.message : String(err)}`
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
spinner.stop("Generation complete")
|
|
92
|
+
|
|
93
|
+
if (errorMessages.length > 0) {
|
|
94
|
+
console.log()
|
|
95
|
+
p.log.error("Errors encountered:")
|
|
96
|
+
for (const msg of errorMessages) {
|
|
97
|
+
console.log(` ${pc.red("•")} ${msg}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log()
|
|
102
|
+
p.note(
|
|
103
|
+
`Success: ${pc.green(successCount.toString())}\n` +
|
|
104
|
+
`Failed: ${pc.red(errorCount.toString())}`,
|
|
105
|
+
"Summary"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if (errorCount > 0) {
|
|
109
|
+
p.outro(pc.red("✖ Generation failed with errors."))
|
|
110
|
+
process.exit(1)
|
|
111
|
+
} else {
|
|
112
|
+
p.outro(pc.green("✓ All hooks generated successfully!"))
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Init
|
|
2
|
+
* Init Command
|
|
3
3
|
*
|
|
4
|
-
* Initializes ic-reactor configuration
|
|
4
|
+
* Initializes ic-reactor.json configuration.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as p from "@clack/prompts"
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
getProjectRoot,
|
|
17
17
|
ensureDir,
|
|
18
18
|
} from "../utils/config.js"
|
|
19
|
-
import type {
|
|
19
|
+
import type { CodegenConfig, CanisterConfig } from "../types.js"
|
|
20
|
+
import { generateClientFile } from "@ic-reactor/codegen"
|
|
20
21
|
|
|
21
22
|
interface InitOptions {
|
|
22
23
|
yes?: boolean
|
|
@@ -42,183 +43,115 @@ export async function initCommand(options: InitOptions) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const projectRoot = getProjectRoot()
|
|
45
|
-
let config:
|
|
46
|
+
let config: CodegenConfig
|
|
46
47
|
|
|
47
48
|
if (options.yes) {
|
|
48
|
-
// Use defaults
|
|
49
49
|
config = { ...DEFAULT_CONFIG }
|
|
50
50
|
if (options.outDir) {
|
|
51
51
|
config.outDir = options.outDir
|
|
52
52
|
}
|
|
53
53
|
} else {
|
|
54
|
-
// Interactive
|
|
54
|
+
// Interactive Setup
|
|
55
|
+
|
|
56
|
+
// Output Directory
|
|
55
57
|
const outDir = await p.text({
|
|
56
|
-
message: "Where should generated
|
|
57
|
-
placeholder: "src/
|
|
58
|
-
defaultValue: "src/
|
|
59
|
-
validate: (value) => {
|
|
60
|
-
if (!value) return "Output directory is required"
|
|
61
|
-
return undefined
|
|
62
|
-
},
|
|
58
|
+
message: "Where should generated files be placed?",
|
|
59
|
+
placeholder: "src/declarations",
|
|
60
|
+
defaultValue: "src/declarations",
|
|
63
61
|
})
|
|
62
|
+
if (p.isCancel(outDir)) process.exit(0)
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Ask if they want to add a canister now
|
|
71
|
-
const addCanister = await p.confirm({
|
|
72
|
-
message: "Would you like to add a canister now?",
|
|
73
|
-
initialValue: true,
|
|
64
|
+
// Client Manager Path
|
|
65
|
+
const clientManagerPath = await p.text({
|
|
66
|
+
message: "Relative path for the client manager import?",
|
|
67
|
+
placeholder: "../../clients",
|
|
68
|
+
defaultValue: "../../clients",
|
|
74
69
|
})
|
|
75
|
-
|
|
76
|
-
if (p.isCancel(addCanister)) {
|
|
77
|
-
p.cancel("Setup cancelled.")
|
|
78
|
-
process.exit(0)
|
|
79
|
-
}
|
|
70
|
+
if (p.isCancel(clientManagerPath)) process.exit(0)
|
|
80
71
|
|
|
81
72
|
config = {
|
|
82
73
|
...DEFAULT_CONFIG,
|
|
83
74
|
outDir: outDir as string,
|
|
75
|
+
clientManagerPath: clientManagerPath as string,
|
|
84
76
|
}
|
|
85
77
|
|
|
78
|
+
// Add initial canister?
|
|
79
|
+
const addCanister = await p.confirm({
|
|
80
|
+
message: "Would you like to configure a canister now?",
|
|
81
|
+
initialValue: true,
|
|
82
|
+
})
|
|
83
|
+
if (p.isCancel(addCanister)) process.exit(0)
|
|
84
|
+
|
|
86
85
|
if (addCanister) {
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
config.canisters[
|
|
86
|
+
const canister = await promptForCanister(projectRoot)
|
|
87
|
+
if (canister) {
|
|
88
|
+
config.canisters[canister.name] = canister
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
// Save config
|
|
93
|
+
// Save config
|
|
95
94
|
const configPath = path.join(projectRoot, CONFIG_FILE_NAME)
|
|
96
95
|
saveConfig(config, configPath)
|
|
97
96
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
// Ensure directories exist
|
|
98
|
+
ensureDir(path.join(projectRoot, config.outDir))
|
|
99
|
+
|
|
100
|
+
// Create default Client Manager
|
|
101
|
+
const clientManagerFile = path.join(projectRoot, "src/clients.ts")
|
|
102
|
+
// Simple heuristic: if clientManagerPath is "../../clients", likely file is src/clients.ts
|
|
103
|
+
// Users can move it, but this gives a good start.
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const createClient = await p.confirm({
|
|
106
|
-
message: "Create a sample client manager at src/lib/clients.ts?",
|
|
105
|
+
if (!fs.existsSync(clientManagerFile)) {
|
|
106
|
+
const createHelpers = await p.confirm({
|
|
107
|
+
message: `Create a default client manager at ${pc.green("src/clients.ts")}?`,
|
|
107
108
|
initialValue: true,
|
|
108
109
|
})
|
|
109
110
|
|
|
110
|
-
if (
|
|
111
|
-
ensureDir(path.dirname(
|
|
112
|
-
fs.writeFileSync(
|
|
113
|
-
p.log.success(`Created ${pc.green("src/
|
|
111
|
+
if (createHelpers === true) {
|
|
112
|
+
ensureDir(path.dirname(clientManagerFile))
|
|
113
|
+
fs.writeFileSync(clientManagerFile, generateClientFile())
|
|
114
|
+
p.log.success(`Created ${pc.green("src/clients.ts")}`)
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
p.log.success(`Created ${pc.green(CONFIG_FILE_NAME)}`)
|
|
118
|
-
p.log.success(`Created ${pc.green(config.outDir)} directory`)
|
|
119
119
|
|
|
120
120
|
console.log()
|
|
121
121
|
p.note(
|
|
122
|
-
`
|
|
123
|
-
|
|
124
|
-
1. ${pc.cyan("Add a canister:")}
|
|
125
|
-
${pc.dim("npx @ic-reactor/cli add")}
|
|
126
|
-
|
|
127
|
-
2. ${pc.cyan("List available methods:")}
|
|
128
|
-
${pc.dim("npx @ic-reactor/cli list -c <canister-name>")}
|
|
129
|
-
|
|
130
|
-
3. ${pc.cyan("Add hooks for specific methods:")}
|
|
131
|
-
${pc.dim("npx @ic-reactor/cli add -c <canister> -m <method>")}`,
|
|
132
|
-
"Getting Started"
|
|
122
|
+
`To generate hooks, run:\n${pc.cyan("npx ic-reactor generate")}`,
|
|
123
|
+
"Next Steps"
|
|
133
124
|
)
|
|
134
125
|
|
|
135
|
-
p.outro(pc.green("✓
|
|
126
|
+
p.outro(pc.green("✓ Setup complete!"))
|
|
136
127
|
}
|
|
137
128
|
|
|
138
129
|
async function promptForCanister(
|
|
139
130
|
projectRoot: string
|
|
140
|
-
): Promise<
|
|
131
|
+
): Promise<CanisterConfig | null> {
|
|
141
132
|
const name = await p.text({
|
|
142
133
|
message: "Canister name",
|
|
143
134
|
placeholder: "backend",
|
|
144
|
-
validate: (
|
|
145
|
-
if (!
|
|
146
|
-
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(
|
|
147
|
-
return "Canister name must start with a letter and contain only letters, numbers, hyphens, and underscores"
|
|
148
|
-
}
|
|
149
|
-
return undefined
|
|
135
|
+
validate: (val) => {
|
|
136
|
+
if (!val) return "Name is required"
|
|
137
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(val)) return "Invalid name format"
|
|
150
138
|
},
|
|
151
139
|
})
|
|
152
|
-
|
|
153
140
|
if (p.isCancel(name)) return null
|
|
154
141
|
|
|
155
142
|
const didFile = await p.text({
|
|
156
143
|
message: "Path to .did file",
|
|
157
|
-
placeholder: "./backend.did",
|
|
158
|
-
validate: (
|
|
159
|
-
if (!
|
|
160
|
-
const fullPath = path.resolve(projectRoot,
|
|
161
|
-
if (!fs.existsSync(fullPath)) {
|
|
162
|
-
return `File not found: ${value}`
|
|
163
|
-
}
|
|
164
|
-
return undefined
|
|
144
|
+
placeholder: "./src/backend/backend.did",
|
|
145
|
+
validate: (val) => {
|
|
146
|
+
if (!val) return "Path is required"
|
|
147
|
+
const fullPath = path.resolve(projectRoot, val)
|
|
148
|
+
if (!fs.existsSync(fullPath)) return `File not found: ${val}`
|
|
165
149
|
},
|
|
166
150
|
})
|
|
167
|
-
|
|
168
151
|
if (p.isCancel(didFile)) return null
|
|
169
152
|
|
|
170
|
-
const clientManagerPath = await p.text({
|
|
171
|
-
message:
|
|
172
|
-
"Import path to your client manager (relative from generated hooks)",
|
|
173
|
-
placeholder: "../../clients",
|
|
174
|
-
defaultValue: "../../clients",
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
if (p.isCancel(clientManagerPath)) return null
|
|
178
|
-
|
|
179
|
-
const useDisplayReactor = await p.confirm({
|
|
180
|
-
message:
|
|
181
|
-
"Use DisplayReactor? (auto-converts bigint → string, Principal → string)",
|
|
182
|
-
initialValue: true,
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
if (p.isCancel(useDisplayReactor)) return null
|
|
186
|
-
|
|
187
153
|
return {
|
|
188
154
|
name: name as string,
|
|
189
|
-
|
|
190
|
-
didFile: didFile as string,
|
|
191
|
-
clientManagerPath: clientManagerPath as string,
|
|
192
|
-
useDisplayReactor: useDisplayReactor as boolean,
|
|
193
|
-
},
|
|
155
|
+
didFile: didFile as string,
|
|
194
156
|
}
|
|
195
157
|
}
|
|
196
|
-
|
|
197
|
-
function getClientManagerTemplate(): string {
|
|
198
|
-
return `/**
|
|
199
|
-
* IC Client Manager
|
|
200
|
-
*
|
|
201
|
-
* This file configures the IC agent and client manager for your application.
|
|
202
|
-
* Customize the agent options based on your environment.
|
|
203
|
-
*/
|
|
204
|
-
|
|
205
|
-
import { ClientManager } from "@ic-reactor/react"
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* The client manager handles agent lifecycle and authentication.
|
|
209
|
-
*
|
|
210
|
-
* Configuration options:
|
|
211
|
-
* - host: IC network host (defaults to process env or mainnet)
|
|
212
|
-
* - identity: Initial identity (optional, can be set later)
|
|
213
|
-
* - verifyQuerySignatures: Verify query signatures (recommended for production)
|
|
214
|
-
*
|
|
215
|
-
* For local development, the agent will automatically detect local replica.
|
|
216
|
-
*/
|
|
217
|
-
export const clientManager = new ClientManager({
|
|
218
|
-
// Uncomment for explicit host configuration:
|
|
219
|
-
// host: process.env.DFX_NETWORK === "local"
|
|
220
|
-
// ? "http://localhost:4943"
|
|
221
|
-
// : "https://icp-api.io",
|
|
222
|
-
})
|
|
223
|
-
`
|
|
224
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -2,42 +2,35 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @ic-reactor/cli
|
|
4
4
|
*
|
|
5
|
-
* CLI tool to generate
|
|
6
|
-
* Gives users full control over generated code - no magic, just scaffolding.
|
|
5
|
+
* CLI tool to generate type-safe React hooks for ICP canisters.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { Command } from "commander"
|
|
10
9
|
import { initCommand } from "./commands/init.js"
|
|
11
|
-
import {
|
|
12
|
-
import { listCommand } from "./commands/list.js"
|
|
10
|
+
import { generateCommand } from "./commands/generate.js"
|
|
13
11
|
import pc from "picocolors"
|
|
12
|
+
import { version } from "../package.json"
|
|
14
13
|
|
|
15
14
|
const program = new Command()
|
|
16
15
|
|
|
17
16
|
program
|
|
18
17
|
.name("ic-reactor")
|
|
19
|
-
.description(
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
.version("3.0.0")
|
|
18
|
+
.description(pc.cyan("🔧 Generate type-safe React hooks for ICP canisters"))
|
|
19
|
+
.version(version)
|
|
23
20
|
|
|
24
21
|
program
|
|
25
22
|
.command("init")
|
|
26
23
|
.description("Initialize ic-reactor configuration in your project")
|
|
27
24
|
.option("-y, --yes", "Skip prompts and use defaults")
|
|
28
|
-
.option("-o, --out-dir <path>", "Output directory for generated
|
|
25
|
+
.option("-o, --out-dir <path>", "Output directory for generated files")
|
|
29
26
|
.action(initCommand)
|
|
30
27
|
|
|
31
28
|
program
|
|
32
|
-
.command("
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.command("list")
|
|
39
|
-
.description("List available methods from a canister")
|
|
40
|
-
.option("-c, --canister <name>", "Canister to list methods from")
|
|
41
|
-
.action(listCommand)
|
|
29
|
+
.command("generate")
|
|
30
|
+
.alias("g")
|
|
31
|
+
.description("Generate hooks from .did files")
|
|
32
|
+
.option("-c, --canister <name>", "Generate for a specific canister only")
|
|
33
|
+
.option("--clean", "Clean output directory before generating")
|
|
34
|
+
.action(generateCommand)
|
|
42
35
|
|
|
43
36
|
program.parse()
|
package/src/types.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI-specific Types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* are now in @ic-reactor/codegen.
|
|
4
|
+
* Most types are now imported from @ic-reactor/codegen to ensure consistency.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
import type { CodegenConfig, CanisterConfig } from "@ic-reactor/codegen"
|
|
9
8
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// Re-export for convenience
|
|
10
|
+
export type { CodegenConfig, CanisterConfig }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CLI arguments for the `init` command
|
|
14
|
+
*/
|
|
15
|
+
export interface InitOptions {
|
|
16
|
+
yes?: boolean
|
|
17
|
+
outDir?: string
|
|
18
|
+
dryRun?: boolean
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
clientManagerPath?: string
|
|
22
|
-
/** Canister configurations */
|
|
23
|
-
canisters: Record<string, CanisterConfig>
|
|
24
|
-
/** Track which hooks have been generated */
|
|
25
|
-
generatedHooks: Record<string, Array<string | HookConfig>>
|
|
21
|
+
/**
|
|
22
|
+
* CLI arguments for the `generate` command
|
|
23
|
+
*/
|
|
24
|
+
export interface GenerateOptions {
|
|
25
|
+
canister?: string
|
|
26
|
+
clean?: boolean
|
|
26
27
|
}
|
package/src/utils/config.ts
CHANGED
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import fs from "node:fs"
|
|
6
6
|
import path from "node:path"
|
|
7
|
-
import type {
|
|
7
|
+
import type { CodegenConfig } from "../types.js"
|
|
8
8
|
|
|
9
9
|
export const CONFIG_FILE_NAME = "ic-reactor.json"
|
|
10
10
|
|
|
11
|
-
export const DEFAULT_CONFIG:
|
|
11
|
+
export const DEFAULT_CONFIG: CodegenConfig = {
|
|
12
12
|
$schema:
|
|
13
13
|
"https://raw.githubusercontent.com/B3Pay/ic-reactor/main/packages/cli/schema.json",
|
|
14
|
-
outDir: "src/
|
|
14
|
+
outDir: "src/declarations",
|
|
15
15
|
canisters: {},
|
|
16
|
-
generatedHooks: {},
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -38,7 +37,7 @@ export function findConfigFile(
|
|
|
38
37
|
/**
|
|
39
38
|
* Load the reactor config file
|
|
40
39
|
*/
|
|
41
|
-
export function loadConfig(configPath?: string):
|
|
40
|
+
export function loadConfig(configPath?: string): CodegenConfig | null {
|
|
42
41
|
const filePath = configPath ?? findConfigFile()
|
|
43
42
|
|
|
44
43
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
@@ -47,7 +46,7 @@ export function loadConfig(configPath?: string): ReactorConfig | null {
|
|
|
47
46
|
|
|
48
47
|
try {
|
|
49
48
|
const content = fs.readFileSync(filePath, "utf-8")
|
|
50
|
-
return JSON.parse(content) as
|
|
49
|
+
return JSON.parse(content) as CodegenConfig
|
|
51
50
|
} catch {
|
|
52
51
|
return null
|
|
53
52
|
}
|
|
@@ -57,7 +56,7 @@ export function loadConfig(configPath?: string): ReactorConfig | null {
|
|
|
57
56
|
* Save the reactor config file
|
|
58
57
|
*/
|
|
59
58
|
export function saveConfig(
|
|
60
|
-
config:
|
|
59
|
+
config: CodegenConfig,
|
|
61
60
|
configPath: string = path.join(process.cwd(), CONFIG_FILE_NAME)
|
|
62
61
|
): void {
|
|
63
62
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
|
|
@@ -65,23 +64,12 @@ export function saveConfig(
|
|
|
65
64
|
|
|
66
65
|
/**
|
|
67
66
|
* Get the project root directory.
|
|
68
|
-
*
|
|
69
|
-
* Priority:
|
|
70
|
-
* 1. Directory containing reactor.config.json (if found)
|
|
71
|
-
* 2. Current working directory (default for new projects)
|
|
72
|
-
*
|
|
73
|
-
* Note: We intentionally don't traverse up looking for package.json
|
|
74
|
-
* as this can cause issues when running in subdirectories or when
|
|
75
|
-
* parent directories have their own package.json files.
|
|
76
67
|
*/
|
|
77
68
|
export function getProjectRoot(): string {
|
|
78
|
-
// First, check if there's a reactor.config.json in the current directory or parents
|
|
79
69
|
const configPath = findConfigFile()
|
|
80
70
|
if (configPath) {
|
|
81
71
|
return path.dirname(configPath)
|
|
82
72
|
}
|
|
83
|
-
|
|
84
|
-
// Default to current working directory for new projects
|
|
85
73
|
return process.cwd()
|
|
86
74
|
}
|
|
87
75
|
|
|
@@ -94,19 +82,8 @@ export function ensureDir(dirPath: string): void {
|
|
|
94
82
|
}
|
|
95
83
|
}
|
|
96
84
|
|
|
97
|
-
/**
|
|
98
|
-
* Check if a file exists
|
|
99
|
-
*/
|
|
100
|
-
export function fileExists(filePath: string): boolean {
|
|
101
|
-
return fs.existsSync(filePath)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Calculate relative path from one file to another
|
|
106
|
-
*/
|
|
107
85
|
export function getRelativePath(from: string, to: string): string {
|
|
108
86
|
const relativePath = path.relative(path.dirname(from), to)
|
|
109
|
-
// Ensure it starts with ./ or ../
|
|
110
87
|
if (!relativePath.startsWith(".")) {
|
|
111
88
|
return "./" + relativePath
|
|
112
89
|
}
|