@cira-app/cli 0.1.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/SKILL.md +78 -0
- package/bin/cira.js +1348 -0
- package/package.json +32 -0
package/bin/cira.js
ADDED
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// dist/config.js
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
var DEFAULT_API_URL = "https://cira-aumitshiv.vercel.app";
|
|
8
|
+
function ciraHome() {
|
|
9
|
+
return process.env["CIRA_HOME"] ?? join(homedir(), ".cira");
|
|
10
|
+
}
|
|
11
|
+
function configPath() {
|
|
12
|
+
return join(ciraHome(), "config.json");
|
|
13
|
+
}
|
|
14
|
+
function readConfig() {
|
|
15
|
+
const apiUrl = process.env["CIRA_API_URL"] ?? DEFAULT_API_URL;
|
|
16
|
+
try {
|
|
17
|
+
const raw = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
18
|
+
return {
|
|
19
|
+
// An explicit environment override always wins over the stored value.
|
|
20
|
+
apiUrl: process.env["CIRA_API_URL"] ?? raw.apiUrl ?? DEFAULT_API_URL,
|
|
21
|
+
...raw.token !== void 0 ? { token: raw.token } : {},
|
|
22
|
+
...raw.email !== void 0 ? { email: raw.email } : {}
|
|
23
|
+
};
|
|
24
|
+
} catch {
|
|
25
|
+
return { apiUrl };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeConfig(config) {
|
|
29
|
+
const path = configPath();
|
|
30
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
31
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
32
|
+
`, { mode: 384 });
|
|
33
|
+
chmodSync(path, 384);
|
|
34
|
+
}
|
|
35
|
+
function clearConfig() {
|
|
36
|
+
try {
|
|
37
|
+
rmSync(configPath());
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// dist/api.js
|
|
43
|
+
var ApiError = class extends Error {
|
|
44
|
+
status;
|
|
45
|
+
constructor(message, status2) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.status = status2;
|
|
48
|
+
this.name = "ApiError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
async function api(path, options = {}) {
|
|
52
|
+
const config = readConfig();
|
|
53
|
+
const token = options.token ?? config.token;
|
|
54
|
+
let response;
|
|
55
|
+
try {
|
|
56
|
+
response = await fetch(`${config.apiUrl}${path}`, {
|
|
57
|
+
method: options.method ?? "GET",
|
|
58
|
+
headers: {
|
|
59
|
+
"content-type": options.raw === void 0 ? "application/json" : "application/octet-stream",
|
|
60
|
+
...options.raw !== void 0 ? { "x-cira-sha": options.raw.sha } : {},
|
|
61
|
+
...token !== void 0 ? { authorization: `Bearer ${token}` } : {}
|
|
62
|
+
},
|
|
63
|
+
...options.raw !== void 0 ? { body: new Uint8Array(options.raw.body) } : options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
throw new ApiError(`Could not reach Cira at ${config.apiUrl}.`, 0);
|
|
67
|
+
}
|
|
68
|
+
if (response.status === 401) {
|
|
69
|
+
throw new ApiError("You are not signed in. Run: cira login", 401);
|
|
70
|
+
}
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const said = await response.json().then((body) => typeof body === "object" && body !== null && "error" in body ? String(body.error) : null).catch(() => null);
|
|
73
|
+
throw new ApiError(said ?? `Cira returned an error (${response.status}).`, response.status);
|
|
74
|
+
}
|
|
75
|
+
return await response.json();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// dist/deploy.js
|
|
79
|
+
import { basename } from "node:path";
|
|
80
|
+
|
|
81
|
+
// dist/files.js
|
|
82
|
+
import { createHash } from "node:crypto";
|
|
83
|
+
import { readFileSync as readFileSync2, readdirSync } from "node:fs";
|
|
84
|
+
import { join as join2, relative, sep } from "node:path";
|
|
85
|
+
|
|
86
|
+
// ../deploy/dist/bundle.js
|
|
87
|
+
var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
88
|
+
"node_modules",
|
|
89
|
+
".git",
|
|
90
|
+
".next",
|
|
91
|
+
".turbo",
|
|
92
|
+
".vercel",
|
|
93
|
+
".cira",
|
|
94
|
+
"dist",
|
|
95
|
+
"build",
|
|
96
|
+
"out",
|
|
97
|
+
"coverage",
|
|
98
|
+
".cache",
|
|
99
|
+
".DS_Store"
|
|
100
|
+
]);
|
|
101
|
+
var EXCLUDED_FILE_PATTERNS = [
|
|
102
|
+
/^\.env($|\.)/,
|
|
103
|
+
/\.log$/,
|
|
104
|
+
/^\.DS_Store$/,
|
|
105
|
+
/\.tsbuildinfo$/,
|
|
106
|
+
/^npm-debug\.log/,
|
|
107
|
+
/^yarn-error\.log/
|
|
108
|
+
];
|
|
109
|
+
function isExcludedDirectory(name) {
|
|
110
|
+
return EXCLUDED_DIRECTORIES.has(name);
|
|
111
|
+
}
|
|
112
|
+
function isExcludedFile(name) {
|
|
113
|
+
if (name === ".env.example")
|
|
114
|
+
return false;
|
|
115
|
+
return EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(name));
|
|
116
|
+
}
|
|
117
|
+
function shouldUpload(relativePath) {
|
|
118
|
+
const segments = relativePath.split("/").filter((s) => s !== "");
|
|
119
|
+
if (segments.length === 0)
|
|
120
|
+
return false;
|
|
121
|
+
for (const segment of segments.slice(0, -1)) {
|
|
122
|
+
if (isExcludedDirectory(segment))
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const name = segments[segments.length - 1];
|
|
126
|
+
if (name === void 0)
|
|
127
|
+
return false;
|
|
128
|
+
if (isExcludedDirectory(name))
|
|
129
|
+
return false;
|
|
130
|
+
return !isExcludedFile(name);
|
|
131
|
+
}
|
|
132
|
+
var MAX_BUNDLE_BYTES = 100 * 1024 * 1024;
|
|
133
|
+
var MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
134
|
+
|
|
135
|
+
// dist/files.js
|
|
136
|
+
function collectFiles(root) {
|
|
137
|
+
const found = [];
|
|
138
|
+
const walk = (dir) => {
|
|
139
|
+
let entries;
|
|
140
|
+
try {
|
|
141
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
142
|
+
} catch {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const full = join2(dir, entry.name);
|
|
147
|
+
const rel = relative(root, full).split(sep).join("/");
|
|
148
|
+
if (!shouldUpload(rel))
|
|
149
|
+
continue;
|
|
150
|
+
if (entry.isDirectory()) {
|
|
151
|
+
walk(full);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!entry.isFile())
|
|
155
|
+
continue;
|
|
156
|
+
try {
|
|
157
|
+
const body = readFileSync2(full);
|
|
158
|
+
found.push({
|
|
159
|
+
path: rel,
|
|
160
|
+
size: body.byteLength,
|
|
161
|
+
sha: createHash("sha1").update(body).digest("hex")
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
walk(root);
|
|
168
|
+
return found.sort((a, b) => a.path.localeCompare(b.path));
|
|
169
|
+
}
|
|
170
|
+
function readFileBody(root, relativePath) {
|
|
171
|
+
return readFileSync2(join2(root, relativePath));
|
|
172
|
+
}
|
|
173
|
+
function readSourceFiles(root, files) {
|
|
174
|
+
const wanted = files.filter((file) => file.path === "package.json" || /\.(ts|tsx|js|jsx|mjs)$/.test(file.path));
|
|
175
|
+
const out = [];
|
|
176
|
+
for (const file of wanted) {
|
|
177
|
+
if (file.size > 4e5)
|
|
178
|
+
continue;
|
|
179
|
+
try {
|
|
180
|
+
out.push({ path: file.path, text: readFileSync2(join2(root, file.path), "utf8") });
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ../extract/dist/extract.js
|
|
188
|
+
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
189
|
+
var LIMITS = {
|
|
190
|
+
routes: 60,
|
|
191
|
+
functions: 80,
|
|
192
|
+
shapes: 60,
|
|
193
|
+
excerptChars: 900,
|
|
194
|
+
shapeChars: 700,
|
|
195
|
+
fileChars: 12e4
|
|
196
|
+
};
|
|
197
|
+
function extractRepo(files) {
|
|
198
|
+
const notes = [];
|
|
199
|
+
const summary = {
|
|
200
|
+
framework: "nextjs",
|
|
201
|
+
packageName: null,
|
|
202
|
+
dependencies: [],
|
|
203
|
+
routes: [],
|
|
204
|
+
functions: [],
|
|
205
|
+
shapes: [],
|
|
206
|
+
notes
|
|
207
|
+
};
|
|
208
|
+
const source = files.filter((f) => isInteresting(f.path));
|
|
209
|
+
const manifest = files.find((f) => f.path === "package.json");
|
|
210
|
+
if (manifest !== void 0) {
|
|
211
|
+
const parsed = readManifest(manifest.text);
|
|
212
|
+
summary.packageName = parsed.name;
|
|
213
|
+
summary.dependencies = parsed.dependencies;
|
|
214
|
+
}
|
|
215
|
+
for (const file of source) {
|
|
216
|
+
if (file.text.length > LIMITS.fileChars) {
|
|
217
|
+
notes.push(`${file.path} was too large to read.`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const route = readRoute(file);
|
|
221
|
+
if (route !== null)
|
|
222
|
+
summary.routes.push(route);
|
|
223
|
+
summary.functions.push(...readFunctions(file));
|
|
224
|
+
summary.shapes.push(...readShapes(file));
|
|
225
|
+
}
|
|
226
|
+
summary.routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
227
|
+
summary.functions.sort((a, b) => a.name.localeCompare(b.name));
|
|
228
|
+
summary.shapes.sort((a, b) => a.name.localeCompare(b.name));
|
|
229
|
+
summary.routes = cap(summary.routes, LIMITS.routes, "routes", notes);
|
|
230
|
+
summary.functions = cap(summary.functions, LIMITS.functions, "functions", notes);
|
|
231
|
+
summary.shapes = cap(summary.shapes, LIMITS.shapes, "shapes", notes);
|
|
232
|
+
return summary;
|
|
233
|
+
}
|
|
234
|
+
function cap(items, limit, what, notes) {
|
|
235
|
+
if (items.length <= limit)
|
|
236
|
+
return items;
|
|
237
|
+
notes.push(`Only the first ${limit} ${what} of ${items.length} are described here.`);
|
|
238
|
+
return items.slice(0, limit);
|
|
239
|
+
}
|
|
240
|
+
function isInteresting(path) {
|
|
241
|
+
if (!/\.(ts|tsx|js|jsx|mjs)$/.test(path))
|
|
242
|
+
return false;
|
|
243
|
+
const noisy = [
|
|
244
|
+
"node_modules/",
|
|
245
|
+
".next/",
|
|
246
|
+
"dist/",
|
|
247
|
+
"build/",
|
|
248
|
+
"out/",
|
|
249
|
+
"coverage/",
|
|
250
|
+
"public/",
|
|
251
|
+
".turbo/",
|
|
252
|
+
".vercel/"
|
|
253
|
+
];
|
|
254
|
+
if (noisy.some((prefix) => path.includes(prefix)))
|
|
255
|
+
return false;
|
|
256
|
+
if (/\.(test|spec)\.[tj]sx?$/.test(path))
|
|
257
|
+
return false;
|
|
258
|
+
if (/\.d\.ts$/.test(path))
|
|
259
|
+
return false;
|
|
260
|
+
if (/(^|\/)(next-env|next\.config|tailwind\.config|postcss\.config)\b/.test(path)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
function readManifest(text) {
|
|
266
|
+
try {
|
|
267
|
+
const pkg = JSON.parse(text);
|
|
268
|
+
return {
|
|
269
|
+
name: typeof pkg.name === "string" ? pkg.name : null,
|
|
270
|
+
// Names only: a version number says nothing about what an app does, and
|
|
271
|
+
// the list is here to hint at the domain (a database client, a payments
|
|
272
|
+
// SDK) rather than to describe a build.
|
|
273
|
+
dependencies: Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).sort()
|
|
274
|
+
};
|
|
275
|
+
} catch {
|
|
276
|
+
return { name: null, dependencies: [] };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function routePathFor(file) {
|
|
280
|
+
const path = file.replace(/^\.\//, "");
|
|
281
|
+
const app = /^(?:src\/)?app\/(.*\/)?route\.[tj]sx?$/.exec(path);
|
|
282
|
+
if (app !== null) {
|
|
283
|
+
const segments = (app[1] ?? "").split("/").filter((s) => s !== "").filter((s) => !(s.startsWith("(") && s.endsWith(")"))).filter((s) => !s.startsWith("_"));
|
|
284
|
+
return `/${segments.join("/")}`.replace(/\/$/, "") || "/";
|
|
285
|
+
}
|
|
286
|
+
const pages = /^(?:src\/)?pages\/(api\/.*)\.[tj]sx?$/.exec(path);
|
|
287
|
+
if (pages !== null) {
|
|
288
|
+
const withoutIndex = (pages[1] ?? "").replace(/\/index$/, "");
|
|
289
|
+
return `/${withoutIndex}`;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function readRoute(file) {
|
|
294
|
+
const path = routePathFor(file.path);
|
|
295
|
+
if (path === null)
|
|
296
|
+
return null;
|
|
297
|
+
const isAppRouter = /^(?:src\/)?app\//.test(file.path.replace(/^\.\//, ""));
|
|
298
|
+
const methods = isAppRouter ? HTTP_METHODS.filter((method) => exportsHandler(file.text, method)) : methodsFromPagesHandler(file.text);
|
|
299
|
+
if (methods.length === 0)
|
|
300
|
+
return null;
|
|
301
|
+
return {
|
|
302
|
+
path,
|
|
303
|
+
methods,
|
|
304
|
+
file: file.path,
|
|
305
|
+
dynamic: path.includes("["),
|
|
306
|
+
doc: leadingDoc(file.text, new RegExp(`(?:async\\s+)?function\\s+${methods[0]}\\b`)) ?? fileDoc(file.text),
|
|
307
|
+
excerpt: trim(stripImports(file.text), LIMITS.excerptChars)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function exportsHandler(text, method) {
|
|
311
|
+
const patterns = [
|
|
312
|
+
new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\s*\\(`),
|
|
313
|
+
new RegExp(`export\\s+(?:const|let|var)\\s+${method}\\s*[:=]`),
|
|
314
|
+
new RegExp(`export\\s*\\{[^}]*\\b${method}\\b[^}]*\\}`)
|
|
315
|
+
];
|
|
316
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
317
|
+
}
|
|
318
|
+
function methodsFromPagesHandler(text) {
|
|
319
|
+
if (!/export\s+default\s/.test(text))
|
|
320
|
+
return [];
|
|
321
|
+
const found = HTTP_METHODS.filter((method) => new RegExp(`method\\s*===?\\s*["'\`]${method}["'\`]`, "i").test(text));
|
|
322
|
+
return found.length > 0 ? found : ["GET", "POST"];
|
|
323
|
+
}
|
|
324
|
+
function readFunctions(file) {
|
|
325
|
+
const serverAction = /^\s*["']use server["']/m.test(file.text);
|
|
326
|
+
const found = [];
|
|
327
|
+
const pattern = /export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)([^{;]*)/g;
|
|
328
|
+
for (const match of file.text.matchAll(pattern)) {
|
|
329
|
+
const name = match[1];
|
|
330
|
+
if (name === void 0)
|
|
331
|
+
continue;
|
|
332
|
+
if (HTTP_METHODS.includes(name))
|
|
333
|
+
continue;
|
|
334
|
+
found.push({
|
|
335
|
+
name,
|
|
336
|
+
file: file.path,
|
|
337
|
+
signature: oneLine(`${name}(${match[2] ?? ""})${match[3] ?? ""}`),
|
|
338
|
+
doc: leadingDoc(file.text, new RegExp(`function\\s+${name}\\b`)),
|
|
339
|
+
serverAction
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return found;
|
|
343
|
+
}
|
|
344
|
+
function readShapes(file) {
|
|
345
|
+
const found = [];
|
|
346
|
+
for (const match of file.text.matchAll(/export\s+interface\s+([A-Za-z_$][\w$]*)\s*(?:extends[^{]*)?\{/g)) {
|
|
347
|
+
const name = match[1];
|
|
348
|
+
if (name === void 0 || match.index === void 0)
|
|
349
|
+
continue;
|
|
350
|
+
found.push({
|
|
351
|
+
name,
|
|
352
|
+
file: file.path,
|
|
353
|
+
kind: "interface",
|
|
354
|
+
text: trim(block(file.text, match.index), LIMITS.shapeChars)
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
for (const match of file.text.matchAll(/export\s+type\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]{0,400});/g)) {
|
|
358
|
+
const name = match[1];
|
|
359
|
+
if (name === void 0)
|
|
360
|
+
continue;
|
|
361
|
+
found.push({
|
|
362
|
+
name,
|
|
363
|
+
file: file.path,
|
|
364
|
+
kind: "type",
|
|
365
|
+
text: oneLine(`type ${name} = ${match[2] ?? ""}`)
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
for (const match of file.text.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*z\s*\.\s*object\s*\(/g)) {
|
|
369
|
+
const name = match[1];
|
|
370
|
+
if (name === void 0 || match.index === void 0)
|
|
371
|
+
continue;
|
|
372
|
+
found.push({
|
|
373
|
+
name,
|
|
374
|
+
file: file.path,
|
|
375
|
+
kind: "zod",
|
|
376
|
+
text: trim(block(file.text, match.index), LIMITS.shapeChars)
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return found;
|
|
380
|
+
}
|
|
381
|
+
function leadingDoc(text, anchor) {
|
|
382
|
+
const at = text.search(anchor);
|
|
383
|
+
if (at === -1)
|
|
384
|
+
return null;
|
|
385
|
+
const before = text.slice(0, at);
|
|
386
|
+
const jsdoc = /\/\*\*([\s\S]*?)\*\/(?:\s*(?:export|default|async|const|let|var))*\s*$/.exec(before);
|
|
387
|
+
if (jsdoc !== null) {
|
|
388
|
+
return oneLine((jsdoc[1] ?? "").replace(/^\s*\*/gm, " ")).slice(0, 400);
|
|
389
|
+
}
|
|
390
|
+
const lines = before.split("\n");
|
|
391
|
+
const comments = [];
|
|
392
|
+
for (let i = lines.length - 2; i >= 0; i -= 1) {
|
|
393
|
+
const line = (lines[i] ?? "").trim();
|
|
394
|
+
if (line.startsWith("//"))
|
|
395
|
+
comments.unshift(line.slice(2).trim());
|
|
396
|
+
else
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
return comments.length > 0 ? comments.join(" ").slice(0, 400) : null;
|
|
400
|
+
}
|
|
401
|
+
function fileDoc(text) {
|
|
402
|
+
const match = /^\s*\/\*\*([\s\S]*?)\*\//.exec(text);
|
|
403
|
+
if (match === null)
|
|
404
|
+
return null;
|
|
405
|
+
return oneLine((match[1] ?? "").replace(/^\s*\*/gm, " ")).slice(0, 400);
|
|
406
|
+
}
|
|
407
|
+
function block(text, start) {
|
|
408
|
+
const open = text.indexOf("{", start);
|
|
409
|
+
if (open === -1)
|
|
410
|
+
return text.slice(start, start + LIMITS.shapeChars);
|
|
411
|
+
let depth = 0;
|
|
412
|
+
for (let i = open; i < text.length && i < open + LIMITS.shapeChars * 2; i += 1) {
|
|
413
|
+
const char = text[i];
|
|
414
|
+
if (char === "{")
|
|
415
|
+
depth += 1;
|
|
416
|
+
else if (char === "}") {
|
|
417
|
+
depth -= 1;
|
|
418
|
+
if (depth === 0)
|
|
419
|
+
return text.slice(start, i + 1);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return text.slice(start, start + LIMITS.shapeChars);
|
|
423
|
+
}
|
|
424
|
+
function stripImports(text) {
|
|
425
|
+
return text.split("\n").filter((line) => !/^\s*import\s/.test(line) && !/^\s*export\s+\*/.test(line)).join("\n").trim();
|
|
426
|
+
}
|
|
427
|
+
function oneLine(text) {
|
|
428
|
+
return text.replace(/\s+/g, " ").trim();
|
|
429
|
+
}
|
|
430
|
+
function trim(text, max) {
|
|
431
|
+
return text.length <= max ? text : `${text.slice(0, max)}
|
|
432
|
+
// ...trimmed`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// dist/project.js
|
|
436
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
437
|
+
import { join as join3 } from "node:path";
|
|
438
|
+
function linkPath(dir) {
|
|
439
|
+
return join3(dir, ".cira", "project.json");
|
|
440
|
+
}
|
|
441
|
+
function readProjectLink(dir = process.cwd()) {
|
|
442
|
+
try {
|
|
443
|
+
const raw = JSON.parse(readFileSync3(linkPath(dir), "utf8"));
|
|
444
|
+
if (typeof raw.appId !== "string" || typeof raw.spaceId !== "string" || typeof raw.spaceSlug !== "string" || typeof raw.appSlug !== "string") {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
return raw;
|
|
448
|
+
} catch {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function writeProjectLink(link, dir = process.cwd()) {
|
|
453
|
+
mkdirSync2(join3(dir, ".cira"), { recursive: true });
|
|
454
|
+
writeFileSync2(linkPath(dir), `${JSON.stringify(link, null, 2)}
|
|
455
|
+
`);
|
|
456
|
+
}
|
|
457
|
+
function detectFramework(dir = process.cwd()) {
|
|
458
|
+
const pkgPath = join3(dir, "package.json");
|
|
459
|
+
if (!existsSync(pkgPath))
|
|
460
|
+
return null;
|
|
461
|
+
try {
|
|
462
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
463
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
464
|
+
return deps["next"] !== void 0 ? "nextjs" : null;
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// dist/ui.js
|
|
471
|
+
var useColour = process.stdout.isTTY === true && process.env["NO_COLOR"] === void 0;
|
|
472
|
+
var wrap = (code) => (text) => useColour ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
473
|
+
var dim = wrap("2");
|
|
474
|
+
var bold = wrap("1");
|
|
475
|
+
var green = wrap("32");
|
|
476
|
+
var red = wrap("31");
|
|
477
|
+
function info(message) {
|
|
478
|
+
process.stdout.write(`${message}
|
|
479
|
+
`);
|
|
480
|
+
}
|
|
481
|
+
function success(message) {
|
|
482
|
+
process.stdout.write(`${green("OK")} ${message}
|
|
483
|
+
`);
|
|
484
|
+
}
|
|
485
|
+
function fail(message) {
|
|
486
|
+
process.stderr.write(`${red("Error")} ${message}
|
|
487
|
+
`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// dist/deploy.js
|
|
491
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
492
|
+
async function deploy(argv = []) {
|
|
493
|
+
const requestedSpace = readFlag(argv, "--space");
|
|
494
|
+
const config = readConfig();
|
|
495
|
+
if (config.token === void 0) {
|
|
496
|
+
fail("Not signed in. Run: cira login");
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
499
|
+
const root = process.cwd();
|
|
500
|
+
info("");
|
|
501
|
+
info(`${dim("Detecting application...")}`);
|
|
502
|
+
const framework = detectFramework(root);
|
|
503
|
+
if (framework === null) {
|
|
504
|
+
fail("This folder is not a Next.js app. Cira deploys Next.js in V1.");
|
|
505
|
+
return 1;
|
|
506
|
+
}
|
|
507
|
+
success("Next.js app");
|
|
508
|
+
const link = readProjectLink(root);
|
|
509
|
+
let me;
|
|
510
|
+
try {
|
|
511
|
+
me = await api("/api/cli/me");
|
|
512
|
+
} catch (error) {
|
|
513
|
+
fail(error instanceof ApiError ? error.message : "Could not reach Cira.");
|
|
514
|
+
return 1;
|
|
515
|
+
}
|
|
516
|
+
if (me.spaces.length === 0) {
|
|
517
|
+
fail("You are not in a space yet. Open Cira and join or create one first.");
|
|
518
|
+
return 1;
|
|
519
|
+
}
|
|
520
|
+
if (requestedSpace !== null && !me.spaces.some((s) => s.slug === requestedSpace)) {
|
|
521
|
+
fail(`You are not in a space called "${requestedSpace}".`);
|
|
522
|
+
info(dim(` You are in: ${me.spaces.map((s) => s.slug).join(", ")}`));
|
|
523
|
+
return 1;
|
|
524
|
+
}
|
|
525
|
+
const spaceSlug = requestedSpace ?? link?.spaceSlug ?? (me.spaces.length === 1 ? me.spaces[0]?.slug : void 0);
|
|
526
|
+
if (spaceSlug === void 0) {
|
|
527
|
+
fail("You are in more than one space, so tell Cira which one to deploy to.");
|
|
528
|
+
info("");
|
|
529
|
+
for (const s of me.spaces) {
|
|
530
|
+
info(` cira deploy --space ${s.slug}${dim(` (${s.name})`)}`);
|
|
531
|
+
}
|
|
532
|
+
info("");
|
|
533
|
+
return 1;
|
|
534
|
+
}
|
|
535
|
+
const files = collectFiles(root);
|
|
536
|
+
if (files.length === 0) {
|
|
537
|
+
fail("There is nothing to deploy in this folder.");
|
|
538
|
+
return 1;
|
|
539
|
+
}
|
|
540
|
+
const bytes = files.reduce((n, f) => n + f.size, 0);
|
|
541
|
+
info(`${dim(`Packaging ${files.length} files (${formatBytes(bytes)})...`)}`);
|
|
542
|
+
for (const file of files) {
|
|
543
|
+
try {
|
|
544
|
+
await api("/api/cli/upload", {
|
|
545
|
+
method: "POST",
|
|
546
|
+
body: void 0,
|
|
547
|
+
raw: { sha: file.sha, body: readFileBody(root, file.path) }
|
|
548
|
+
});
|
|
549
|
+
} catch (error) {
|
|
550
|
+
fail(error instanceof ApiError ? `${error.message} while uploading ${file.path}` : `Could not upload ${file.path}.`);
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
success("Uploaded");
|
|
555
|
+
info(`${dim(`Deploying to ${spaceSlug}...`)}`);
|
|
556
|
+
let started;
|
|
557
|
+
try {
|
|
558
|
+
started = await api("/api/cli/deploy", {
|
|
559
|
+
method: "POST",
|
|
560
|
+
body: {
|
|
561
|
+
spaceSlug,
|
|
562
|
+
appName: link === null ? prettyName(basename(root)) : basename(root),
|
|
563
|
+
appId: link?.appId ?? null,
|
|
564
|
+
files
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
} catch (error) {
|
|
568
|
+
fail(error instanceof ApiError ? error.message : "The deploy could not be started.");
|
|
569
|
+
return 1;
|
|
570
|
+
}
|
|
571
|
+
writeProjectLink({
|
|
572
|
+
appId: started.appId,
|
|
573
|
+
spaceId: "",
|
|
574
|
+
spaceSlug: started.spaceSlug,
|
|
575
|
+
appSlug: started.appSlug
|
|
576
|
+
}, root);
|
|
577
|
+
info(`${dim("Analyzing capabilities...")}`);
|
|
578
|
+
const analysis = api("/api/cli/capabilities", {
|
|
579
|
+
method: "POST",
|
|
580
|
+
body: {
|
|
581
|
+
appId: started.appId,
|
|
582
|
+
summary: extractRepo(readSourceFiles(root, files))
|
|
583
|
+
}
|
|
584
|
+
}).catch((error) => error instanceof Error ? error : new Error("failed"));
|
|
585
|
+
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
586
|
+
let last = "";
|
|
587
|
+
while (Date.now() < deadline) {
|
|
588
|
+
await sleep(3e3);
|
|
589
|
+
let status2;
|
|
590
|
+
try {
|
|
591
|
+
status2 = await api(`/api/cli/deploy/status?id=${encodeURIComponent(started.deploymentId)}`);
|
|
592
|
+
} catch {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (status2.status !== last) {
|
|
596
|
+
info(dim(` ${status2.status}...`));
|
|
597
|
+
last = status2.status;
|
|
598
|
+
}
|
|
599
|
+
if (status2.status === "live") {
|
|
600
|
+
info("");
|
|
601
|
+
success("Deployed");
|
|
602
|
+
info("");
|
|
603
|
+
info(` ${bold(`${config.apiUrl}/${started.spaceSlug}/${started.appSlug}`)}`);
|
|
604
|
+
info("");
|
|
605
|
+
info(dim(" Only you can see it. Give people access from that page."));
|
|
606
|
+
await reportCapabilities(await analysis);
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
if (status2.status === "failed" || status2.status === "removed") {
|
|
610
|
+
info("");
|
|
611
|
+
fail("The deploy did not finish. Open the app in Cira to see why.");
|
|
612
|
+
return 1;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
fail("Timed out waiting for the deploy to finish.");
|
|
616
|
+
return 1;
|
|
617
|
+
}
|
|
618
|
+
function prettyName(folder) {
|
|
619
|
+
const cleaned = folder.replace(/[-_]+/g, " ").trim();
|
|
620
|
+
return cleaned === "" ? "App" : cleaned.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
621
|
+
}
|
|
622
|
+
function formatBytes(n) {
|
|
623
|
+
if (n < 1024)
|
|
624
|
+
return `${n} B`;
|
|
625
|
+
if (n < 1024 * 1024)
|
|
626
|
+
return `${Math.round(n / 1024)} KB`;
|
|
627
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
628
|
+
}
|
|
629
|
+
function readFlag(argv, flag) {
|
|
630
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
631
|
+
const arg = argv[i];
|
|
632
|
+
if (arg === flag)
|
|
633
|
+
return argv[i + 1] ?? null;
|
|
634
|
+
if (arg !== void 0 && arg.startsWith(`${flag}=`)) {
|
|
635
|
+
return arg.slice(flag.length + 1);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
async function reportCapabilities(result) {
|
|
641
|
+
if (result instanceof Error) {
|
|
642
|
+
info("");
|
|
643
|
+
info(dim(` Capabilities were not analyzed: ${result.message}`));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (result.detected.length === 0) {
|
|
647
|
+
info("");
|
|
648
|
+
info(dim(" No capabilities detected in this app."));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
info("");
|
|
652
|
+
info(`${bold("Capabilities")}`);
|
|
653
|
+
info("");
|
|
654
|
+
for (const capability of result.detected) {
|
|
655
|
+
const mark = capability.risk === "read" ? " " : "!";
|
|
656
|
+
info(` ${mark} ${bold(capability.name)}`);
|
|
657
|
+
info(` ${dim(capability.description)}`);
|
|
658
|
+
}
|
|
659
|
+
info("");
|
|
660
|
+
const parts = [];
|
|
661
|
+
if (result.enabled > 0)
|
|
662
|
+
parts.push(`${result.enabled} enabled`);
|
|
663
|
+
if (result.review > 0)
|
|
664
|
+
parts.push(`${result.review} awaiting review`);
|
|
665
|
+
info(` ${parts.join(", ")}`);
|
|
666
|
+
if (result.review > 0) {
|
|
667
|
+
info(dim(" Anything that writes stays off until someone turns it on."));
|
|
668
|
+
}
|
|
669
|
+
info("");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// dist/login.js
|
|
673
|
+
import { hostname } from "node:os";
|
|
674
|
+
|
|
675
|
+
// dist/skill/command.js
|
|
676
|
+
import { createInterface } from "node:readline/promises";
|
|
677
|
+
|
|
678
|
+
// dist/skill/index.js
|
|
679
|
+
import { homedir as homedir2 } from "node:os";
|
|
680
|
+
|
|
681
|
+
// ../cira-skill/dist/index.js
|
|
682
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
683
|
+
var SKILL_PATH = new URL("../SKILL.md", import.meta.url);
|
|
684
|
+
var MANIFEST = new URL("../package.json", import.meta.url);
|
|
685
|
+
function canonicalSkill() {
|
|
686
|
+
const source = readFileSync4(SKILL_PATH, "utf8");
|
|
687
|
+
const parsed = splitFrontmatter(source);
|
|
688
|
+
return {
|
|
689
|
+
name: parsed.fields["name"] ?? "cira",
|
|
690
|
+
description: parsed.fields["description"] ?? "Build and deploy to Cira.",
|
|
691
|
+
body: parsed.body,
|
|
692
|
+
source,
|
|
693
|
+
version: packageVersion()
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function packageVersion() {
|
|
697
|
+
try {
|
|
698
|
+
const pkg = JSON.parse(readFileSync4(MANIFEST, "utf8"));
|
|
699
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
700
|
+
} catch {
|
|
701
|
+
return "0.0.0";
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function splitFrontmatter(source) {
|
|
705
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(source);
|
|
706
|
+
if (match === null)
|
|
707
|
+
return { fields: {}, body: source.trim() };
|
|
708
|
+
const fields = {};
|
|
709
|
+
for (const line of (match[1] ?? "").split("\n")) {
|
|
710
|
+
const at = line.indexOf(":");
|
|
711
|
+
if (at === -1)
|
|
712
|
+
continue;
|
|
713
|
+
const key = line.slice(0, at).trim();
|
|
714
|
+
const value = line.slice(at + 1).trim();
|
|
715
|
+
if (key !== "")
|
|
716
|
+
fields[key] = value;
|
|
717
|
+
}
|
|
718
|
+
return { fields, body: source.slice(match[0].length).trim() };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// dist/update/state.js
|
|
722
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
723
|
+
import { join as join4 } from "node:path";
|
|
724
|
+
function readUpdateState() {
|
|
725
|
+
return read("update-state.json", {});
|
|
726
|
+
}
|
|
727
|
+
function writeUpdateState(state) {
|
|
728
|
+
write("update-state.json", state);
|
|
729
|
+
}
|
|
730
|
+
function readSkillState() {
|
|
731
|
+
const state = read("skill-state.json", { targets: {} });
|
|
732
|
+
return { ...state, targets: state.targets ?? {} };
|
|
733
|
+
}
|
|
734
|
+
function writeSkillState(state) {
|
|
735
|
+
write("skill-state.json", state);
|
|
736
|
+
}
|
|
737
|
+
function read(name, fallback) {
|
|
738
|
+
try {
|
|
739
|
+
return {
|
|
740
|
+
...fallback,
|
|
741
|
+
...JSON.parse(readFileSync5(join4(ciraHome(), name), "utf8"))
|
|
742
|
+
};
|
|
743
|
+
} catch {
|
|
744
|
+
return fallback;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function write(name, value) {
|
|
748
|
+
try {
|
|
749
|
+
mkdirSync3(ciraHome(), { recursive: true, mode: 448 });
|
|
750
|
+
writeFileSync3(join4(ciraHome(), name), `${JSON.stringify(value, null, 2)}
|
|
751
|
+
`);
|
|
752
|
+
} catch {
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// dist/skill/agents.js
|
|
757
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
758
|
+
import { join as join6 } from "node:path";
|
|
759
|
+
|
|
760
|
+
// dist/skill/installer.js
|
|
761
|
+
import { mkdirSync as mkdirSync4, existsSync as existsSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
762
|
+
import { delimiter, join as join5 } from "node:path";
|
|
763
|
+
function sharedSkillsDir(env) {
|
|
764
|
+
return join5(env.home, ".agents", "skills");
|
|
765
|
+
}
|
|
766
|
+
function writeSkill(skillsDir, skill) {
|
|
767
|
+
const directory = join5(skillsDir, skill.name);
|
|
768
|
+
mkdirSync4(directory, { recursive: true });
|
|
769
|
+
const path = join5(directory, "SKILL.md");
|
|
770
|
+
writeFileSync4(path, `${skill.source.trimEnd()}
|
|
771
|
+
`);
|
|
772
|
+
return { ok: true, where: path };
|
|
773
|
+
}
|
|
774
|
+
function onPath(env, command) {
|
|
775
|
+
return env.path.some((dir) => [command, `${command}.exe`, `${command}.cmd`].some((name) => dir === "" ? false : existsSync2(join5(dir, name))));
|
|
776
|
+
}
|
|
777
|
+
function systemPath() {
|
|
778
|
+
return (process.env["PATH"] ?? "").split(delimiter).filter((p) => p !== "");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// dist/skill/agents.js
|
|
782
|
+
function claudeCode(env) {
|
|
783
|
+
const home = process.env["CLAUDE_CONFIG_DIR"] ?? join6(env.home, ".claude");
|
|
784
|
+
return {
|
|
785
|
+
id: "claude-code",
|
|
786
|
+
name: "Claude Code",
|
|
787
|
+
detect: () => Promise.resolve(existsSync3(home) || onPath(env, "claude")),
|
|
788
|
+
install: (skill) => Promise.resolve(writeSkill(join6(home, "skills"), skill))
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function codex(env) {
|
|
792
|
+
const home = process.env["CODEX_HOME"] ?? join6(env.home, ".codex");
|
|
793
|
+
return {
|
|
794
|
+
id: "codex",
|
|
795
|
+
name: "Codex",
|
|
796
|
+
detect: () => Promise.resolve(existsSync3(home) || onPath(env, "codex")),
|
|
797
|
+
install: (skill) => Promise.resolve(writeSkill(sharedSkillsDir(env), skill))
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function pi(env) {
|
|
801
|
+
return {
|
|
802
|
+
id: "pi",
|
|
803
|
+
name: "Pi",
|
|
804
|
+
detect: () => Promise.resolve(existsSync3(join6(env.home, ".pi")) || onPath(env, "pi")),
|
|
805
|
+
install: (skill) => Promise.resolve(writeSkill(sharedSkillsDir(env), skill))
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function geminiCli(env) {
|
|
809
|
+
return {
|
|
810
|
+
id: "gemini-cli",
|
|
811
|
+
name: "Gemini CLI",
|
|
812
|
+
detect: () => Promise.resolve(existsSync3(join6(env.home, ".gemini")) || onPath(env, "gemini")),
|
|
813
|
+
install: (skill) => Promise.resolve(writeSkill(sharedSkillsDir(env), skill))
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// dist/skill/cursor.js
|
|
818
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
819
|
+
import { join as join7 } from "node:path";
|
|
820
|
+
function cursor(env) {
|
|
821
|
+
return {
|
|
822
|
+
id: "cursor",
|
|
823
|
+
name: "Cursor",
|
|
824
|
+
detect: () => Promise.resolve(existsSync4(join7(env.home, ".cursor")) || existsSync4(join7(env.home, ".config", "Cursor")) || existsSync4(join7(env.home, "Library", "Application Support", "Cursor")) || // On WSL the editor is installed on the Windows side: its binary is
|
|
825
|
+
// reachable while its configuration directory is not.
|
|
826
|
+
onPath(env, "cursor")),
|
|
827
|
+
install: (skill) => {
|
|
828
|
+
if (!isProject(env.cwd)) {
|
|
829
|
+
return Promise.resolve({
|
|
830
|
+
ok: false,
|
|
831
|
+
why: "run this from a project folder - Cursor rules live in the project"
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const directory = join7(env.cwd, ".cursor", "rules");
|
|
835
|
+
mkdirSync5(directory, { recursive: true });
|
|
836
|
+
const path = join7(directory, `${skill.name}.mdc`);
|
|
837
|
+
const frontmatter = [
|
|
838
|
+
"---",
|
|
839
|
+
`description: ${skill.description}`,
|
|
840
|
+
"globs:",
|
|
841
|
+
"alwaysApply: false",
|
|
842
|
+
"---"
|
|
843
|
+
].join("\n");
|
|
844
|
+
writeFileSync5(path, `${frontmatter}
|
|
845
|
+
|
|
846
|
+
${skill.body.trim()}
|
|
847
|
+
`);
|
|
848
|
+
return Promise.resolve({ ok: true, where: path });
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function isProject(dir) {
|
|
853
|
+
return existsSync4(join7(dir, "package.json")) || existsSync4(join7(dir, ".git")) || existsSync4(join7(dir, "pyproject.toml")) || existsSync4(join7(dir, "go.mod"));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// dist/skill/index.js
|
|
857
|
+
function installers(env = currentEnv()) {
|
|
858
|
+
return [claudeCode(env), codex(env), pi(env), geminiCli(env), cursor(env)];
|
|
859
|
+
}
|
|
860
|
+
function currentEnv() {
|
|
861
|
+
return { home: homedir2(), cwd: process.cwd(), path: systemPath() };
|
|
862
|
+
}
|
|
863
|
+
async function detectAgents(env = currentEnv()) {
|
|
864
|
+
const found = [];
|
|
865
|
+
for (const installer of installers(env)) {
|
|
866
|
+
if (await installer.detect())
|
|
867
|
+
found.push(installer);
|
|
868
|
+
}
|
|
869
|
+
return found;
|
|
870
|
+
}
|
|
871
|
+
async function installSkill(agents, autoUpdate = true) {
|
|
872
|
+
const skill = canonicalSkill();
|
|
873
|
+
const reports = [];
|
|
874
|
+
for (const agent of agents) {
|
|
875
|
+
try {
|
|
876
|
+
reports.push({ agent: agent.name, result: await agent.install(skill) });
|
|
877
|
+
} catch (error) {
|
|
878
|
+
reports.push({
|
|
879
|
+
agent: agent.name,
|
|
880
|
+
result: {
|
|
881
|
+
ok: false,
|
|
882
|
+
why: error instanceof Error ? error.message : "could not write the skill"
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
remember(agents, reports, autoUpdate);
|
|
888
|
+
return reports;
|
|
889
|
+
}
|
|
890
|
+
function remember(agents, reports, autoUpdate) {
|
|
891
|
+
const installed = new Set(reports.filter((r) => r.result.ok).map((r) => r.agent));
|
|
892
|
+
if (installed.size === 0)
|
|
893
|
+
return;
|
|
894
|
+
const state = readSkillState();
|
|
895
|
+
const targets = { ...state.targets };
|
|
896
|
+
for (const agent of agents) {
|
|
897
|
+
if (!installed.has(agent.name))
|
|
898
|
+
continue;
|
|
899
|
+
targets[agent.id] = { installed: true, autoUpdate };
|
|
900
|
+
}
|
|
901
|
+
writeSkillState({ ...state, skillVersion: canonicalSkill().version, targets });
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// dist/skill/command.js
|
|
905
|
+
async function skillCommand(argv) {
|
|
906
|
+
const action = argv[0] ?? "install";
|
|
907
|
+
if (action !== "install") {
|
|
908
|
+
fail(`Unknown skill command: ${action}`);
|
|
909
|
+
info(dim(" Try: cira skill install"));
|
|
910
|
+
return 1;
|
|
911
|
+
}
|
|
912
|
+
const agents = await detectAgents();
|
|
913
|
+
info("");
|
|
914
|
+
info(bold("Cira Skill"));
|
|
915
|
+
info("");
|
|
916
|
+
if (agents.length === 0) {
|
|
917
|
+
info(dim(" No supported coding agents detected."));
|
|
918
|
+
info(dim(" Cira installs into Claude Code, Codex, Pi, Gemini CLI and Cursor."));
|
|
919
|
+
info("");
|
|
920
|
+
return 0;
|
|
921
|
+
}
|
|
922
|
+
report(await installSkill(agents));
|
|
923
|
+
return 0;
|
|
924
|
+
}
|
|
925
|
+
async function offerSkill() {
|
|
926
|
+
const agents = await detectAgents();
|
|
927
|
+
if (agents.length === 0) {
|
|
928
|
+
info("");
|
|
929
|
+
info(dim(" No supported coding agents detected."));
|
|
930
|
+
info(dim(" You can install the Cira Skill later with: cira skill install"));
|
|
931
|
+
info("");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
info("");
|
|
935
|
+
info(" Coding agents detected:");
|
|
936
|
+
info("");
|
|
937
|
+
for (const agent of agents)
|
|
938
|
+
info(` ${agent.name}`);
|
|
939
|
+
info("");
|
|
940
|
+
if (!await confirm(" Install the Cira Skill and keep it updated? [Y/n] ")) {
|
|
941
|
+
info("");
|
|
942
|
+
info(dim(" Left alone. Install later with: cira skill install"));
|
|
943
|
+
info("");
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
info("");
|
|
947
|
+
report(await installSkill(agents));
|
|
948
|
+
}
|
|
949
|
+
function report(reports) {
|
|
950
|
+
const seen = /* @__PURE__ */ new Set();
|
|
951
|
+
for (const entry of reports) {
|
|
952
|
+
if (!entry.result.ok) {
|
|
953
|
+
info(` - ${entry.agent} ${dim(entry.result.why)}`);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const where = entry.result.where;
|
|
957
|
+
success(`${entry.agent} ${dim(seen.has(where) ? "(same file)" : where)}`);
|
|
958
|
+
seen.add(where);
|
|
959
|
+
}
|
|
960
|
+
const installed = reports.filter((r) => r.result.ok).length;
|
|
961
|
+
info("");
|
|
962
|
+
if (installed > 0)
|
|
963
|
+
info(` ${bold("You're ready.")}`);
|
|
964
|
+
info("");
|
|
965
|
+
}
|
|
966
|
+
async function confirm(question) {
|
|
967
|
+
if (!process.stdin.isTTY) {
|
|
968
|
+
info(dim(" Not a terminal, so nothing was installed."));
|
|
969
|
+
info(dim(" Run: cira skill install"));
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
973
|
+
try {
|
|
974
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
975
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
976
|
+
} finally {
|
|
977
|
+
rl.close();
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// dist/login.js
|
|
982
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
983
|
+
async function login() {
|
|
984
|
+
const config = readConfig();
|
|
985
|
+
let start;
|
|
986
|
+
try {
|
|
987
|
+
start = await api("/api/cli/auth/start", {
|
|
988
|
+
method: "POST",
|
|
989
|
+
body: { label: `${hostname()} CLI` }
|
|
990
|
+
});
|
|
991
|
+
} catch (error) {
|
|
992
|
+
fail(error instanceof ApiError ? error.message : "Could not start sign-in.");
|
|
993
|
+
return 1;
|
|
994
|
+
}
|
|
995
|
+
const url = `${config.apiUrl}${start.verifyPath}?code=${start.userCode}`;
|
|
996
|
+
info("");
|
|
997
|
+
info(` Open ${bold(url)}`);
|
|
998
|
+
info(` and confirm this code: ${bold(start.userCode)}`);
|
|
999
|
+
info("");
|
|
1000
|
+
info(dim(" Waiting for you to approve..."));
|
|
1001
|
+
const deadline = Date.now() + start.expiresInSeconds * 1e3;
|
|
1002
|
+
while (Date.now() < deadline) {
|
|
1003
|
+
await sleep2(start.intervalSeconds * 1e3);
|
|
1004
|
+
let poll;
|
|
1005
|
+
try {
|
|
1006
|
+
poll = await api("/api/cli/auth/poll", {
|
|
1007
|
+
method: "POST",
|
|
1008
|
+
body: { deviceCode: start.deviceCode }
|
|
1009
|
+
});
|
|
1010
|
+
} catch {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (poll.status === "approved" && poll.token !== void 0) {
|
|
1014
|
+
writeConfig({
|
|
1015
|
+
apiUrl: config.apiUrl,
|
|
1016
|
+
token: poll.token,
|
|
1017
|
+
...poll.user !== void 0 ? { email: poll.user.email } : {}
|
|
1018
|
+
});
|
|
1019
|
+
info("");
|
|
1020
|
+
success(`Signed in as ${poll.user?.email ?? "your account"}`);
|
|
1021
|
+
await offerSkill();
|
|
1022
|
+
return 0;
|
|
1023
|
+
}
|
|
1024
|
+
if (poll.status === "expired" || poll.status === "claimed") {
|
|
1025
|
+
info("");
|
|
1026
|
+
fail("That sign-in expired. Run cira login again.");
|
|
1027
|
+
return 1;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
info("");
|
|
1031
|
+
fail("Timed out waiting for approval. Run cira login again.");
|
|
1032
|
+
return 1;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// dist/update/index.js
|
|
1036
|
+
import { execFile } from "node:child_process";
|
|
1037
|
+
import { promisify } from "node:util";
|
|
1038
|
+
|
|
1039
|
+
// dist/update/check.js
|
|
1040
|
+
import { get } from "node:https";
|
|
1041
|
+
|
|
1042
|
+
// dist/update/version.js
|
|
1043
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1044
|
+
var MANIFESTS = ["../package.json", "../../package.json"];
|
|
1045
|
+
function currentVersion() {
|
|
1046
|
+
for (const candidate of MANIFESTS) {
|
|
1047
|
+
try {
|
|
1048
|
+
const pkg = JSON.parse(readFileSync6(new URL(candidate, import.meta.url), "utf8"));
|
|
1049
|
+
if (pkg.name !== "@cira-app/cli")
|
|
1050
|
+
continue;
|
|
1051
|
+
if (typeof pkg.version === "string")
|
|
1052
|
+
return pkg.version;
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return "0.0.0";
|
|
1057
|
+
}
|
|
1058
|
+
function isNewer(candidate, current) {
|
|
1059
|
+
const a = parse(candidate);
|
|
1060
|
+
const b = parse(current);
|
|
1061
|
+
if (a === null || b === null)
|
|
1062
|
+
return false;
|
|
1063
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1064
|
+
const left = a.parts[i] ?? 0;
|
|
1065
|
+
const right = b.parts[i] ?? 0;
|
|
1066
|
+
if (left !== right)
|
|
1067
|
+
return left > right;
|
|
1068
|
+
}
|
|
1069
|
+
return b.prerelease && !a.prerelease;
|
|
1070
|
+
}
|
|
1071
|
+
function parse(value) {
|
|
1072
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+](.*))?$/.exec(value.trim());
|
|
1073
|
+
if (match === null)
|
|
1074
|
+
return null;
|
|
1075
|
+
return {
|
|
1076
|
+
parts: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
1077
|
+
prerelease: match[4] !== void 0 && match[4] !== ""
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// dist/update/check.js
|
|
1082
|
+
var REGISTRY = "https://registry.npmjs.org/@cira-app%2Fcli/latest";
|
|
1083
|
+
var CHECK_TIMEOUT_MS = 2500;
|
|
1084
|
+
var TTL_MS = 8 * 60 * 60 * 1e3;
|
|
1085
|
+
var fetchLatestVersion = (signal, { detached }) => new Promise((resolve) => {
|
|
1086
|
+
const done = (version) => resolve(version);
|
|
1087
|
+
const request = get(REGISTRY, { signal, headers: { accept: "application/json" } }, (response) => {
|
|
1088
|
+
if (response.statusCode !== 200) {
|
|
1089
|
+
response.resume();
|
|
1090
|
+
done(null);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
let body = "";
|
|
1094
|
+
response.setEncoding("utf8");
|
|
1095
|
+
response.on("data", (chunk) => {
|
|
1096
|
+
body += chunk;
|
|
1097
|
+
if (body.length > 2e5)
|
|
1098
|
+
request.destroy();
|
|
1099
|
+
});
|
|
1100
|
+
response.on("end", () => {
|
|
1101
|
+
try {
|
|
1102
|
+
const version = JSON.parse(body).version;
|
|
1103
|
+
done(typeof version === "string" ? version : null);
|
|
1104
|
+
} catch {
|
|
1105
|
+
done(null);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
if (detached)
|
|
1110
|
+
request.on("socket", (socket) => socket.unref());
|
|
1111
|
+
request.setTimeout(CHECK_TIMEOUT_MS, () => request.destroy());
|
|
1112
|
+
request.on("error", () => done(null));
|
|
1113
|
+
});
|
|
1114
|
+
async function checkForUpdate(options = {}) {
|
|
1115
|
+
const current = currentVersion();
|
|
1116
|
+
const state = readUpdateState();
|
|
1117
|
+
const now = options.now ?? Date.now();
|
|
1118
|
+
if (options.force !== true && !expired(state.lastCheckedAt, now)) {
|
|
1119
|
+
const cached = state.latestVersion ?? null;
|
|
1120
|
+
return {
|
|
1121
|
+
current,
|
|
1122
|
+
latest: cached,
|
|
1123
|
+
hasUpdate: cached !== null && isNewer(cached, current)
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
writeUpdateState({ ...state, lastCheckedAt: new Date(now).toISOString() });
|
|
1127
|
+
const fetchLatest = options.fetchLatest ?? fetchLatestVersion;
|
|
1128
|
+
const controller = new AbortController();
|
|
1129
|
+
options.signal?.addEventListener("abort", () => controller.abort());
|
|
1130
|
+
const latest = await fetchLatest(controller.signal, {
|
|
1131
|
+
detached: options.force !== true
|
|
1132
|
+
});
|
|
1133
|
+
if (latest !== null) {
|
|
1134
|
+
writeUpdateState({
|
|
1135
|
+
...readUpdateState(),
|
|
1136
|
+
lastCheckedAt: new Date(now).toISOString(),
|
|
1137
|
+
latestVersion: latest
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
return { current, latest, hasUpdate: latest !== null && isNewer(latest, current) };
|
|
1141
|
+
}
|
|
1142
|
+
function expired(lastCheckedAt, now) {
|
|
1143
|
+
if (lastCheckedAt === void 0)
|
|
1144
|
+
return true;
|
|
1145
|
+
const at = Date.parse(lastCheckedAt);
|
|
1146
|
+
return Number.isNaN(at) || now - at >= TTL_MS;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// dist/update/index.js
|
|
1150
|
+
var run = promisify(execFile);
|
|
1151
|
+
var npmUpdater = async (version) => {
|
|
1152
|
+
await run("npm", ["install", "-g", `@cira-app/cli@${version}`], {
|
|
1153
|
+
timeout: 12e4,
|
|
1154
|
+
windowsHide: true
|
|
1155
|
+
});
|
|
1156
|
+
};
|
|
1157
|
+
async function updateCommand(options = {}) {
|
|
1158
|
+
info("");
|
|
1159
|
+
info(dim("Checking for updates..."));
|
|
1160
|
+
const check = await checkForUpdate({ force: true, ...pick(options, "fetchLatest") });
|
|
1161
|
+
if (!check.hasUpdate || check.latest === null) {
|
|
1162
|
+
info("");
|
|
1163
|
+
success("Cira is up to date.");
|
|
1164
|
+
info("");
|
|
1165
|
+
return 0;
|
|
1166
|
+
}
|
|
1167
|
+
info("");
|
|
1168
|
+
info(bold("CLI"));
|
|
1169
|
+
info(` ${check.current} \u2192 ${check.latest}`);
|
|
1170
|
+
info("");
|
|
1171
|
+
info(dim("Updating..."));
|
|
1172
|
+
info("");
|
|
1173
|
+
try {
|
|
1174
|
+
await (options.updateCli ?? npmUpdater)(check.latest);
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
fail("CLI update failed.");
|
|
1177
|
+
info(dim(" Your existing installation was left unchanged."));
|
|
1178
|
+
info(dim(` ${error instanceof Error ? error.message : "npm could not run."}`));
|
|
1179
|
+
info("");
|
|
1180
|
+
return 1;
|
|
1181
|
+
}
|
|
1182
|
+
success("CLI updated");
|
|
1183
|
+
const results = await syncInstalledSkills(check.latest, options.agents);
|
|
1184
|
+
for (const result of results) {
|
|
1185
|
+
if (result.ok)
|
|
1186
|
+
success(`Cira Skill updated for ${result.agent}`);
|
|
1187
|
+
else
|
|
1188
|
+
fail(`Cira Skill update failed for ${result.agent} ${dim(result.why)}`);
|
|
1189
|
+
}
|
|
1190
|
+
const state = readUpdateState();
|
|
1191
|
+
writeUpdateState({ ...state, lastNotifiedVersion: check.latest });
|
|
1192
|
+
info("");
|
|
1193
|
+
if (results.some((r) => !r.ok)) {
|
|
1194
|
+
info(" Cira is updated, but some Skill installations need attention.");
|
|
1195
|
+
} else {
|
|
1196
|
+
success("Cira is up to date.");
|
|
1197
|
+
}
|
|
1198
|
+
info("");
|
|
1199
|
+
return 0;
|
|
1200
|
+
}
|
|
1201
|
+
async function syncInstalledSkills(version, agents = installers()) {
|
|
1202
|
+
const state = readSkillState();
|
|
1203
|
+
const skill = canonicalSkill();
|
|
1204
|
+
const results = [];
|
|
1205
|
+
for (const agent of agents) {
|
|
1206
|
+
const target = state.targets[agent.id];
|
|
1207
|
+
if (target === void 0 || !target.installed || !target.autoUpdate)
|
|
1208
|
+
continue;
|
|
1209
|
+
try {
|
|
1210
|
+
const outcome = await agent.install(skill);
|
|
1211
|
+
results.push({
|
|
1212
|
+
agent: agent.name,
|
|
1213
|
+
ok: outcome.ok,
|
|
1214
|
+
why: outcome.ok ? outcome.where : outcome.why
|
|
1215
|
+
});
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
results.push({
|
|
1218
|
+
agent: agent.name,
|
|
1219
|
+
ok: false,
|
|
1220
|
+
why: error instanceof Error ? error.message : "could not write the skill"
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (results.some((r) => r.ok)) {
|
|
1225
|
+
writeSkillState({ ...state, skillVersion: version });
|
|
1226
|
+
}
|
|
1227
|
+
return results;
|
|
1228
|
+
}
|
|
1229
|
+
function beginUpdateCheck(options = {}) {
|
|
1230
|
+
const controller = new AbortController();
|
|
1231
|
+
const result = checkForUpdate({
|
|
1232
|
+
signal: controller.signal,
|
|
1233
|
+
...pick(options, "fetchLatest")
|
|
1234
|
+
}).then((check) => ({ latest: check.latest, hasUpdate: check.hasUpdate })).catch(() => null);
|
|
1235
|
+
return { result, cancel: () => controller.abort() };
|
|
1236
|
+
}
|
|
1237
|
+
var GRACE_MS = 100;
|
|
1238
|
+
async function finishUpdateCheck(pending) {
|
|
1239
|
+
const timeout = new Promise((resolve) => {
|
|
1240
|
+
const timer = setTimeout(() => resolve(null), GRACE_MS);
|
|
1241
|
+
timer.unref?.();
|
|
1242
|
+
});
|
|
1243
|
+
const check = await Promise.race([pending.result, timeout]);
|
|
1244
|
+
pending.cancel();
|
|
1245
|
+
if (check === null || !check.hasUpdate || check.latest === null)
|
|
1246
|
+
return;
|
|
1247
|
+
const state = readUpdateState();
|
|
1248
|
+
if (state.lastNotifiedVersion === check.latest)
|
|
1249
|
+
return;
|
|
1250
|
+
info("");
|
|
1251
|
+
info(` Cira ${bold(check.latest)} is available.`);
|
|
1252
|
+
info(dim(" Run `cira update`."));
|
|
1253
|
+
writeUpdateState({ ...state, lastNotifiedVersion: check.latest });
|
|
1254
|
+
}
|
|
1255
|
+
function pick(source, key) {
|
|
1256
|
+
return source[key] === void 0 ? {} : { [key]: source[key] };
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// dist/index.js
|
|
1260
|
+
var USAGE = `
|
|
1261
|
+
${bold("cira")} - deploy software to your company
|
|
1262
|
+
|
|
1263
|
+
${bold("Commands")}
|
|
1264
|
+
deploy Deploy this folder to your company
|
|
1265
|
+
--space <slug> which space, when you are in more than one
|
|
1266
|
+
login Connect this machine to your Cira account
|
|
1267
|
+
skill install Add the Cira Skill to your coding agents
|
|
1268
|
+
update Update Cira and the Skill copies you approved
|
|
1269
|
+
logout Forget the stored credential
|
|
1270
|
+
whoami Show who you are signed in as
|
|
1271
|
+
status Show what this folder is linked to
|
|
1272
|
+
|
|
1273
|
+
${bold("Environment")}
|
|
1274
|
+
CIRA_API_URL Point at a different Cira (default: production)
|
|
1275
|
+
CIRA_HOME Where the credential is stored (default: ~/.cira)
|
|
1276
|
+
`;
|
|
1277
|
+
async function whoami() {
|
|
1278
|
+
const config = readConfig();
|
|
1279
|
+
if (config.token === void 0) {
|
|
1280
|
+
fail("Not signed in. Run: cira login");
|
|
1281
|
+
return 1;
|
|
1282
|
+
}
|
|
1283
|
+
try {
|
|
1284
|
+
const me = await api("/api/cli/me");
|
|
1285
|
+
info(`${bold(me.user.name)} ${dim(`<${me.user.email}>`)}`);
|
|
1286
|
+
if (me.spaces.length === 0) {
|
|
1287
|
+
info(dim(" No spaces yet."));
|
|
1288
|
+
} else {
|
|
1289
|
+
for (const space of me.spaces) {
|
|
1290
|
+
info(` ${space.name} ${dim(`(${space.role})`)}`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return 0;
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
fail(error instanceof ApiError ? error.message : "Could not reach Cira.");
|
|
1296
|
+
return 1;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function status() {
|
|
1300
|
+
const framework = detectFramework();
|
|
1301
|
+
const link = readProjectLink();
|
|
1302
|
+
info(`${bold("Folder")} ${process.cwd()}`);
|
|
1303
|
+
info(`${bold("Framework")} ${framework ?? dim("not a supported project")}`);
|
|
1304
|
+
info(link === null ? `${bold("Linked")} ${dim("not linked to an app yet")}` : `${bold("Linked")} ${link.spaceSlug}/${link.appSlug}`);
|
|
1305
|
+
return 0;
|
|
1306
|
+
}
|
|
1307
|
+
async function main() {
|
|
1308
|
+
const command = process.argv[2];
|
|
1309
|
+
switch (command) {
|
|
1310
|
+
case "deploy":
|
|
1311
|
+
return deploy(process.argv.slice(3));
|
|
1312
|
+
case "login":
|
|
1313
|
+
return login();
|
|
1314
|
+
case "skill":
|
|
1315
|
+
return skillCommand(process.argv.slice(3));
|
|
1316
|
+
case "update":
|
|
1317
|
+
return updateCommand();
|
|
1318
|
+
case "logout":
|
|
1319
|
+
clearConfig();
|
|
1320
|
+
success("Signed out.");
|
|
1321
|
+
return 0;
|
|
1322
|
+
case "whoami":
|
|
1323
|
+
return whoami();
|
|
1324
|
+
case "status":
|
|
1325
|
+
return status();
|
|
1326
|
+
case void 0:
|
|
1327
|
+
case "help":
|
|
1328
|
+
case "--help":
|
|
1329
|
+
case "-h":
|
|
1330
|
+
info(USAGE);
|
|
1331
|
+
return 0;
|
|
1332
|
+
default:
|
|
1333
|
+
fail(`Unknown command: ${command}`);
|
|
1334
|
+
info(USAGE);
|
|
1335
|
+
return 1;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
var passive = process.argv[2] === "update" ? null : beginUpdateCheck();
|
|
1339
|
+
main().then(async (code) => {
|
|
1340
|
+
process.exitCode = code;
|
|
1341
|
+
if (passive !== null)
|
|
1342
|
+
await finishUpdateCheck(passive);
|
|
1343
|
+
}).catch(async (error) => {
|
|
1344
|
+
fail(error instanceof Error ? error.message : "Something went wrong.");
|
|
1345
|
+
process.exitCode = 1;
|
|
1346
|
+
if (passive !== null)
|
|
1347
|
+
await finishUpdateCheck(passive);
|
|
1348
|
+
});
|