@raccolta/cli 0.1.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 +12 -0
- package/bin/postinstall.js +16 -0
- package/bin/rac.js +483 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Raccolta CLI
|
|
2
|
+
|
|
3
|
+
Install:
|
|
4
|
+
|
|
5
|
+
```powershell
|
|
6
|
+
npm install -g @raccolta/cli
|
|
7
|
+
rac login --endpoint https://api.raccolta.dev/api
|
|
8
|
+
rac tasks
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Create an API key in the dashboard under Settings > API. Keys use the
|
|
12
|
+
`rac_` prefix and are stored locally in `~/.raccolta/config.json`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
|
|
4
|
+
const green = (text) => (useColor ? `\x1b[32m${text}\x1b[0m` : text)
|
|
5
|
+
const bold = (text) => (useColor ? `\x1b[1m${text}\x1b[0m` : text)
|
|
6
|
+
const dim = (text) => (useColor ? `\x1b[2m${text}\x1b[0m` : text)
|
|
7
|
+
|
|
8
|
+
console.log('')
|
|
9
|
+
console.log(`${green('[ok]')} ${bold('Raccolta CLI installed')}`)
|
|
10
|
+
console.log('')
|
|
11
|
+
console.log('Next steps:')
|
|
12
|
+
console.log(` 1. Create an API key in ${bold('Raccolta -> Settings -> API')}`)
|
|
13
|
+
console.log(` 2. Run ${bold('rac login')}`)
|
|
14
|
+
console.log(` 3. Run ${bold('rac tasks')}`)
|
|
15
|
+
console.log('')
|
|
16
|
+
console.log(dim('Docs: run rac help'))
|
package/bin/rac.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import readline from 'node:readline/promises'
|
|
6
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
7
|
+
|
|
8
|
+
const VERSION = '0.1.1'
|
|
9
|
+
const DEFAULT_ENDPOINT = 'https://api.raccolta.dev/api'
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), '.raccolta')
|
|
11
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
12
|
+
const INSTRUCTIONS_START = '<!-- RACCOLTA CLI INSTRUCTIONS START -->'
|
|
13
|
+
const INSTRUCTIONS_END = '<!-- RACCOLTA CLI INSTRUCTIONS END -->'
|
|
14
|
+
|
|
15
|
+
const COMMANDS = new Set(['login', 'tasks', 'whoami', 'logout', 'config', 'help'])
|
|
16
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g
|
|
17
|
+
const USE_COLOR = Boolean(output.isTTY) && !process.env.NO_COLOR
|
|
18
|
+
const ANSI = {
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
bold: '\x1b[1m',
|
|
21
|
+
dim: '\x1b[2m',
|
|
22
|
+
gray: '\x1b[90m',
|
|
23
|
+
red: '\x1b[31m',
|
|
24
|
+
green: '\x1b[32m',
|
|
25
|
+
yellow: '\x1b[33m',
|
|
26
|
+
blue: '\x1b[34m',
|
|
27
|
+
magenta: '\x1b[35m',
|
|
28
|
+
cyan: '\x1b[36m',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
main().catch((error) => {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
33
|
+
console.error(`Error: ${message}`)
|
|
34
|
+
process.exitCode = 1
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const parsed = parseArgs(process.argv.slice(2))
|
|
39
|
+
const command = parsed.command || 'help'
|
|
40
|
+
|
|
41
|
+
if (parsed.flags.version) {
|
|
42
|
+
console.log(`rac ${VERSION}`)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (parsed.flags.help || command === 'help') {
|
|
47
|
+
printHelp()
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!COMMANDS.has(command)) {
|
|
52
|
+
throw new Error(`Unknown command "${command}". Run rac help.`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (command === 'login') return login(parsed)
|
|
56
|
+
if (command === 'tasks') return tasks(parsed)
|
|
57
|
+
if (command === 'whoami') return whoami()
|
|
58
|
+
if (command === 'logout') return logout()
|
|
59
|
+
if (command === 'config') return printConfig()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseArgs(argv) {
|
|
63
|
+
const flags = {}
|
|
64
|
+
const positionals = []
|
|
65
|
+
let command = ''
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
68
|
+
const arg = argv[i]
|
|
69
|
+
if (!arg.startsWith('-') && !command) {
|
|
70
|
+
command = arg
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (!arg.startsWith('-')) {
|
|
74
|
+
positionals.push(arg)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const normalized = arg.replace(/^--?/, '')
|
|
79
|
+
const [rawKey, inlineValue] = normalized.split('=', 2)
|
|
80
|
+
const key = rawKey.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
|
|
81
|
+
const takesValue = ['endpoint', 'key', 'limit'].includes(key)
|
|
82
|
+
|
|
83
|
+
if (takesValue) {
|
|
84
|
+
flags[key] = inlineValue ?? argv[++i]
|
|
85
|
+
} else {
|
|
86
|
+
flags[key] = true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { command, flags, positionals }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function printHelp() {
|
|
94
|
+
console.log(`${style('Raccolta CLI', 'bold')} ${VERSION}
|
|
95
|
+
|
|
96
|
+
Usage:
|
|
97
|
+
rac login [--endpoint https://api.raccolta.dev/api] [--key rac_...]
|
|
98
|
+
rac tasks [--limit 50]
|
|
99
|
+
rac whoami
|
|
100
|
+
rac logout
|
|
101
|
+
|
|
102
|
+
Install:
|
|
103
|
+
npm install -g @raccolta/cli
|
|
104
|
+
|
|
105
|
+
Create API keys in the dashboard under Settings > API.`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function login({ flags }) {
|
|
109
|
+
const existing = await readConfig().catch(() => null)
|
|
110
|
+
const rl = createReadline()
|
|
111
|
+
try {
|
|
112
|
+
console.log(style('Raccolta CLI Setup', 'bold'))
|
|
113
|
+
console.log('')
|
|
114
|
+
|
|
115
|
+
const endpointDefault = normalizeEndpoint(
|
|
116
|
+
flags.endpoint ||
|
|
117
|
+
process.env.RAC_ENDPOINT ||
|
|
118
|
+
process.env.RAC_API_URL ||
|
|
119
|
+
existing?.endpoint ||
|
|
120
|
+
DEFAULT_ENDPOINT,
|
|
121
|
+
)
|
|
122
|
+
const endpointInput = flags.endpoint
|
|
123
|
+
? endpointDefault
|
|
124
|
+
: await ask(rl, `Endpoint (${endpointDefault}): `)
|
|
125
|
+
const endpoint = normalizeEndpoint(endpointInput || endpointDefault)
|
|
126
|
+
|
|
127
|
+
const apiKey =
|
|
128
|
+
flags.key ||
|
|
129
|
+
process.env.RAC_API_KEY ||
|
|
130
|
+
(await ask(rl, 'API key (from Settings > API): '))
|
|
131
|
+
|
|
132
|
+
if (!apiKey?.trim()) throw new Error('API key is required')
|
|
133
|
+
if (!apiKey.trim().startsWith('rac_')) throw new Error('Raccolta API keys must start with rac_')
|
|
134
|
+
|
|
135
|
+
console.log(`${style('[ok]', 'green')} Endpoint ${style(endpoint, 'dim')}`)
|
|
136
|
+
console.log(`${style('[ok]', 'green')} Validating API key...`)
|
|
137
|
+
|
|
138
|
+
const validation = await apiJson(endpoint, '/cli/auth/validate', apiKey.trim())
|
|
139
|
+
if (!validation?.ok) throw new Error('API key validation failed')
|
|
140
|
+
|
|
141
|
+
await writeConfig({
|
|
142
|
+
endpoint,
|
|
143
|
+
apiKey: apiKey.trim(),
|
|
144
|
+
profile: validation.profile,
|
|
145
|
+
workspace: validation.workspace,
|
|
146
|
+
updatedAt: new Date().toISOString(),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const profileName = validation.profile?.name || validation.profile?.email || validation.profile?.id || 'Raccolta user'
|
|
150
|
+
const workspaceName = validation.workspace?.name || 'Raccolta Workspace'
|
|
151
|
+
console.log(`${style('[ok]', 'green')} Connected profile ${profileName} to workspace ${workspaceName}`)
|
|
152
|
+
|
|
153
|
+
const shouldAppend =
|
|
154
|
+
flags.yes ||
|
|
155
|
+
(!flags.noInstructions &&
|
|
156
|
+
(await confirm(rl, 'Append agent instructions to CLAUDE.md and AGENTS.md in this directory?', true)))
|
|
157
|
+
|
|
158
|
+
if (shouldAppend) {
|
|
159
|
+
const written = await appendAgentInstructions(process.cwd(), validation.workspace)
|
|
160
|
+
for (const item of written) {
|
|
161
|
+
console.log(`${style('[ok]', 'green')} ${item.action} ${item.file}`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log('')
|
|
166
|
+
console.log(`Next: run ${style('rac tasks', 'bold')} to see your tasks.`)
|
|
167
|
+
} finally {
|
|
168
|
+
rl.close()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function tasks({ flags }) {
|
|
173
|
+
const config = await requireConfig()
|
|
174
|
+
const limit = parseLimit(flags.limit)
|
|
175
|
+
console.log(`${style('[ok]', 'green')} Fetching tasks...`)
|
|
176
|
+
const data = await apiJson(
|
|
177
|
+
config.endpoint,
|
|
178
|
+
`/cli/tasks?limit=${encodeURIComponent(String(limit))}`,
|
|
179
|
+
config.apiKey,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const workspaceName = data?.workspace?.name || config.workspace?.name || 'Raccolta Workspace'
|
|
183
|
+
const rows = Array.isArray(data?.tasks) ? data.tasks : []
|
|
184
|
+
console.log('')
|
|
185
|
+
console.log(style(`${workspaceName} Tasks`, 'bold'))
|
|
186
|
+
console.log('')
|
|
187
|
+
|
|
188
|
+
if (rows.length === 0) {
|
|
189
|
+
console.log(style('No tasks found.', 'dim'))
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
printTable(
|
|
194
|
+
rows.map((task) => ({
|
|
195
|
+
Code: task.code || '',
|
|
196
|
+
Title: task.title || '',
|
|
197
|
+
Status: task.status || '',
|
|
198
|
+
Priority: formatPriority(task.priority),
|
|
199
|
+
Agent: task.agent || 'None',
|
|
200
|
+
})),
|
|
201
|
+
)
|
|
202
|
+
console.log('')
|
|
203
|
+
console.log(style(`${rows.length} ${rows.length === 1 ? 'task' : 'tasks'}`, 'dim'))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function whoami() {
|
|
207
|
+
const config = await requireConfig()
|
|
208
|
+
const validation = await apiJson(config.endpoint, '/cli/auth/validate', config.apiKey)
|
|
209
|
+
const profileName = validation.profile?.name || validation.profile?.email || validation.profile?.id || 'Raccolta user'
|
|
210
|
+
const workspaceName = validation.workspace?.name || 'Raccolta Workspace'
|
|
211
|
+
console.log(`Profile: ${profileName}`)
|
|
212
|
+
console.log(`Workspace: ${workspaceName}`)
|
|
213
|
+
console.log(`Endpoint: ${config.endpoint}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function logout() {
|
|
217
|
+
await fs.rm(CONFIG_FILE, { force: true })
|
|
218
|
+
console.log('Removed local Raccolta CLI credentials.')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function printConfig() {
|
|
222
|
+
const config = await readConfig().catch(() => null)
|
|
223
|
+
console.log(`Config file: ${CONFIG_FILE}`)
|
|
224
|
+
if (!config) {
|
|
225
|
+
console.log('Status: not logged in')
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
console.log(`Endpoint: ${config.endpoint}`)
|
|
229
|
+
console.log(`Workspace: ${config.workspace?.name || config.workspace?.id || 'Unknown'}`)
|
|
230
|
+
console.log(`Profile: ${config.profile?.name || config.profile?.email || config.profile?.id || 'Unknown'}`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function apiJson(endpoint, route, apiKey) {
|
|
234
|
+
const response = await fetch(`${endpoint}${route}`, {
|
|
235
|
+
method: 'GET',
|
|
236
|
+
headers: {
|
|
237
|
+
Accept: 'application/json',
|
|
238
|
+
Authorization: `Bearer ${apiKey}`,
|
|
239
|
+
},
|
|
240
|
+
signal: AbortSignal.timeout(20000),
|
|
241
|
+
})
|
|
242
|
+
const text = await response.text()
|
|
243
|
+
let payload = null
|
|
244
|
+
if (text) {
|
|
245
|
+
try {
|
|
246
|
+
payload = JSON.parse(text)
|
|
247
|
+
} catch {
|
|
248
|
+
payload = { error: text.slice(0, 300) }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
if (response.status === 401 && route.startsWith('/cli/')) {
|
|
253
|
+
throw new Error('API key rejected. Create a fresh key in Raccolta Settings > API, then run rac login again. If the fresh key is rejected too, deploy the latest Beast backend.')
|
|
254
|
+
}
|
|
255
|
+
throw new Error(payload?.error || `Request failed with ${response.status}`)
|
|
256
|
+
}
|
|
257
|
+
return payload
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeEndpoint(value) {
|
|
261
|
+
let endpoint = String(value || DEFAULT_ENDPOINT).trim()
|
|
262
|
+
if (!endpoint) endpoint = DEFAULT_ENDPOINT
|
|
263
|
+
if (!/^https?:\/\//i.test(endpoint)) endpoint = `https://${endpoint}`
|
|
264
|
+
endpoint = endpoint.replace(/\/+$/, '')
|
|
265
|
+
if (!endpoint.endsWith('/api')) endpoint = `${endpoint}/api`
|
|
266
|
+
return endpoint
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseLimit(value) {
|
|
270
|
+
if (value == null || value === '') return 50
|
|
271
|
+
const parsed = Number.parseInt(value, 10)
|
|
272
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error('limit must be a positive number')
|
|
273
|
+
return Math.min(parsed, 200)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatPriority(value) {
|
|
277
|
+
const text = String(value || '').trim().toLowerCase()
|
|
278
|
+
if (!text || text === 'none') return 'None'
|
|
279
|
+
return `${text.charAt(0).toUpperCase()}${text.slice(1)}`
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function printTable(rows) {
|
|
283
|
+
const headers = Object.keys(rows[0] || {})
|
|
284
|
+
const widths = tableWidths(headers, rows)
|
|
285
|
+
|
|
286
|
+
console.log(formatRow(headers, headers, widths, true))
|
|
287
|
+
console.log(style(widths.map((width) => '-'.repeat(width)).join(' '), 'gray'))
|
|
288
|
+
for (const row of rows) {
|
|
289
|
+
console.log(formatRow(headers.map((header) => row[header] ?? ''), headers, widths))
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function tableWidths(headers, rows) {
|
|
294
|
+
const terminalWidth = Math.max(72, Math.min(Number(output.columns || 100), 140))
|
|
295
|
+
const gapWidth = Math.max(0, headers.length - 1) * 2
|
|
296
|
+
const fixedHeaders = headers.filter((header) => header !== 'Title')
|
|
297
|
+
const fixedCaps = {
|
|
298
|
+
Code: 10,
|
|
299
|
+
Status: 14,
|
|
300
|
+
Priority: 10,
|
|
301
|
+
Agent: 14,
|
|
302
|
+
}
|
|
303
|
+
const fixedWidths = Object.fromEntries(
|
|
304
|
+
fixedHeaders.map((header) => {
|
|
305
|
+
const max = Math.max(
|
|
306
|
+
visibleWidth(header),
|
|
307
|
+
...rows.map((row) => visibleWidth(String(row[header] ?? ''))),
|
|
308
|
+
)
|
|
309
|
+
return [header, Math.min(max, fixedCaps[header] || max)]
|
|
310
|
+
}),
|
|
311
|
+
)
|
|
312
|
+
const fixedTotal = Object.values(fixedWidths).reduce((sum, width) => sum + width, 0)
|
|
313
|
+
const titleMax = Math.max(24, terminalWidth - fixedTotal - gapWidth)
|
|
314
|
+
const titleContent = Math.max(
|
|
315
|
+
visibleWidth('Title'),
|
|
316
|
+
...rows.map((row) => visibleWidth(String(row.Title ?? ''))),
|
|
317
|
+
)
|
|
318
|
+
const titleWidth = Math.min(titleContent, titleMax)
|
|
319
|
+
|
|
320
|
+
return headers.map((header) => (header === 'Title' ? titleWidth : fixedWidths[header] || visibleWidth(header)))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function formatRow(values, headers, widths, isHeader = false) {
|
|
324
|
+
return values
|
|
325
|
+
.map((value, index) => {
|
|
326
|
+
const header = headers[index]
|
|
327
|
+
const width = widths[index]
|
|
328
|
+
const text = truncateVisible(String(value), width)
|
|
329
|
+
const padded = padVisible(text, width)
|
|
330
|
+
return isHeader ? style(padded, 'dim') : styleTableCell(header, padded, value)
|
|
331
|
+
})
|
|
332
|
+
.join(' ')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function styleTableCell(header, padded, rawValue) {
|
|
336
|
+
const value = String(rawValue || '').trim().toLowerCase()
|
|
337
|
+
if (header === 'Status') {
|
|
338
|
+
if (value === 'shipped') return style(padded, 'green')
|
|
339
|
+
if (value === 'in progress') return style(padded, 'yellow')
|
|
340
|
+
if (value === 'todo') return style(padded, 'cyan')
|
|
341
|
+
if (value === 'declined' || value === 'archive' || value === 'archived') return style(padded, 'gray')
|
|
342
|
+
if (value === 'requests') return style(padded, 'blue')
|
|
343
|
+
}
|
|
344
|
+
if (header === 'Priority') {
|
|
345
|
+
if (value === 'urgent') return style(padded, 'red')
|
|
346
|
+
if (value === 'high') return style(padded, 'yellow')
|
|
347
|
+
if (value === 'medium') return style(padded, 'blue')
|
|
348
|
+
if (value === 'low') return style(padded, 'green')
|
|
349
|
+
if (value === 'none') return style(padded, 'dim')
|
|
350
|
+
}
|
|
351
|
+
if (header === 'Agent' && value === 'none') return style(padded, 'dim')
|
|
352
|
+
return padded
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function style(text, color) {
|
|
356
|
+
if (!USE_COLOR || !ANSI[color]) return text
|
|
357
|
+
return `${ANSI[color]}${text}${ANSI.reset}`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function stripAnsi(value) {
|
|
361
|
+
return String(value ?? '').replace(ANSI_RE, '')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function visibleWidth(value) {
|
|
365
|
+
let width = 0
|
|
366
|
+
for (const char of Array.from(stripAnsi(value))) {
|
|
367
|
+
width += charWidth(char)
|
|
368
|
+
}
|
|
369
|
+
return width
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function padVisible(value, width) {
|
|
373
|
+
return `${value}${' '.repeat(Math.max(0, width - visibleWidth(value)))}`
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function truncateVisible(value, width) {
|
|
377
|
+
const text = stripAnsi(value)
|
|
378
|
+
if (visibleWidth(text) <= width) return text
|
|
379
|
+
if (width <= 1) return Array.from(text).slice(0, width).join('')
|
|
380
|
+
|
|
381
|
+
let next = ''
|
|
382
|
+
let used = 0
|
|
383
|
+
const limit = width - 1
|
|
384
|
+
for (const char of Array.from(text)) {
|
|
385
|
+
const charDisplayWidth = charWidth(char)
|
|
386
|
+
if (used + charDisplayWidth > limit) break
|
|
387
|
+
next += char
|
|
388
|
+
used += charDisplayWidth
|
|
389
|
+
}
|
|
390
|
+
return `${next}.`
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function charWidth(char) {
|
|
394
|
+
const code = char.codePointAt(0)
|
|
395
|
+
if (code == null) return 0
|
|
396
|
+
if (
|
|
397
|
+
(code >= 0x0300 && code <= 0x036f) ||
|
|
398
|
+
(code >= 0x1ab0 && code <= 0x1aff) ||
|
|
399
|
+
(code >= 0x1dc0 && code <= 0x1dff) ||
|
|
400
|
+
(code >= 0x20d0 && code <= 0x20ff) ||
|
|
401
|
+
(code >= 0xfe20 && code <= 0xfe2f)
|
|
402
|
+
) {
|
|
403
|
+
return 0
|
|
404
|
+
}
|
|
405
|
+
if (
|
|
406
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
407
|
+
code === 0x2329 ||
|
|
408
|
+
code === 0x232a ||
|
|
409
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
410
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
411
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
412
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
413
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
414
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
415
|
+
(code >= 0xffe0 && code <= 0xffe6)
|
|
416
|
+
) {
|
|
417
|
+
return 2
|
|
418
|
+
}
|
|
419
|
+
return 1
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function readConfig() {
|
|
423
|
+
const text = await fs.readFile(CONFIG_FILE, 'utf8')
|
|
424
|
+
return JSON.parse(text)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function requireConfig() {
|
|
428
|
+
const config = await readConfig().catch(() => null)
|
|
429
|
+
if (!config?.endpoint || !config?.apiKey) {
|
|
430
|
+
throw new Error('Not logged in. Run rac login first.')
|
|
431
|
+
}
|
|
432
|
+
return config
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function writeConfig(config) {
|
|
436
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true })
|
|
437
|
+
await fs.writeFile(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 })
|
|
438
|
+
await fs.chmod(CONFIG_FILE, 0o600).catch(() => {})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function createReadline() {
|
|
442
|
+
return readline.createInterface({ input, output })
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function ask(rl, question) {
|
|
446
|
+
return (await rl.question(question)).trim()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function confirm(rl, question, defaultYes = true) {
|
|
450
|
+
const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] '
|
|
451
|
+
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase()
|
|
452
|
+
if (!answer) return defaultYes
|
|
453
|
+
return ['y', 'yes', 'j', 'ja'].includes(answer)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function appendAgentInstructions(directory, workspace = {}) {
|
|
457
|
+
const workspaceName = workspace?.name || 'Raccolta Workspace'
|
|
458
|
+
const block = `${INSTRUCTIONS_START}
|
|
459
|
+
# Raccolta CLI
|
|
460
|
+
|
|
461
|
+
This project is connected to the Raccolta workspace "${workspaceName}".
|
|
462
|
+
|
|
463
|
+
- Use \`rac tasks\` to list current Raccolta tasks/cards.
|
|
464
|
+
- Use task codes like \`RAC-12\` when referencing work from the CLI.
|
|
465
|
+
- Do not print or commit Raccolta API keys. Local credentials are stored in \`~/.raccolta/config.json\`.
|
|
466
|
+
${INSTRUCTIONS_END}
|
|
467
|
+
`
|
|
468
|
+
|
|
469
|
+
const results = []
|
|
470
|
+
for (const file of ['CLAUDE.md', 'AGENTS.md']) {
|
|
471
|
+
const filePath = path.join(directory, file)
|
|
472
|
+
const existing = await fs.readFile(filePath, 'utf8').catch(() => '')
|
|
473
|
+
if (existing.includes(INSTRUCTIONS_START)) {
|
|
474
|
+
results.push({ file, action: 'Instructions already present in' })
|
|
475
|
+
continue
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const next = existing.trimEnd() ? `${existing.trimEnd()}\n\n${block}` : `${block}`
|
|
479
|
+
await fs.writeFile(filePath, next, 'utf8')
|
|
480
|
+
results.push({ file, action: existing ? 'Updated' : 'Created' })
|
|
481
|
+
}
|
|
482
|
+
return results
|
|
483
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@raccolta/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Raccolta command line integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"postinstall": "node ./bin/postinstall.js"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"rac": "bin/rac.js"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"license": "UNLICENSED"
|
|
23
|
+
}
|