@kigo-dev/sindri 0.1.4 → 0.1.6
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 +25 -1
- package/dist/index.js +698 -179
- package/manifest.json +18 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3262,8 +3262,8 @@ var require_utils = __commonJS({
|
|
|
3262
3262
|
}
|
|
3263
3263
|
return ind;
|
|
3264
3264
|
}
|
|
3265
|
-
function removeDotSegments(
|
|
3266
|
-
let input2 =
|
|
3265
|
+
function removeDotSegments(path11) {
|
|
3266
|
+
let input2 = path11;
|
|
3267
3267
|
const output2 = [];
|
|
3268
3268
|
let nextSlash = -1;
|
|
3269
3269
|
let len = 0;
|
|
@@ -3672,8 +3672,8 @@ var require_schemes = __commonJS({
|
|
|
3672
3672
|
}
|
|
3673
3673
|
if (wsComponent.resourceName) {
|
|
3674
3674
|
const queryIndex = wsComponent.resourceName.indexOf("?");
|
|
3675
|
-
const
|
|
3676
|
-
wsComponent.path =
|
|
3675
|
+
const path11 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
|
|
3676
|
+
wsComponent.path = path11 && path11 !== "/" ? path11 : void 0;
|
|
3677
3677
|
wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
|
|
3678
3678
|
wsComponent.resourceName = void 0;
|
|
3679
3679
|
}
|
|
@@ -3732,7 +3732,7 @@ var require_schemes = __commonJS({
|
|
|
3732
3732
|
urnComponent.nss = (uuidComponent.uuid || "").toLowerCase();
|
|
3733
3733
|
return urnComponent;
|
|
3734
3734
|
}
|
|
3735
|
-
var
|
|
3735
|
+
var http3 = (
|
|
3736
3736
|
/** @type {SchemeHandler} */
|
|
3737
3737
|
{
|
|
3738
3738
|
scheme: "http",
|
|
@@ -3745,7 +3745,7 @@ var require_schemes = __commonJS({
|
|
|
3745
3745
|
/** @type {SchemeHandler} */
|
|
3746
3746
|
{
|
|
3747
3747
|
scheme: "https",
|
|
3748
|
-
domainHost:
|
|
3748
|
+
domainHost: http3.domainHost,
|
|
3749
3749
|
parse: httpParse,
|
|
3750
3750
|
serialize: httpSerialize
|
|
3751
3751
|
}
|
|
@@ -3789,7 +3789,7 @@ var require_schemes = __commonJS({
|
|
|
3789
3789
|
var SCHEMES = (
|
|
3790
3790
|
/** @type {Record<SchemeName, SchemeHandler>} */
|
|
3791
3791
|
{
|
|
3792
|
-
http:
|
|
3792
|
+
http: http3,
|
|
3793
3793
|
https,
|
|
3794
3794
|
ws,
|
|
3795
3795
|
wss,
|
|
@@ -7185,12 +7185,12 @@ var require_dist = __commonJS({
|
|
|
7185
7185
|
throw new Error(`Unknown format "${name}"`);
|
|
7186
7186
|
return f;
|
|
7187
7187
|
};
|
|
7188
|
-
function addFormats(ajv, list,
|
|
7188
|
+
function addFormats(ajv, list, fs11, exportName) {
|
|
7189
7189
|
var _a3;
|
|
7190
7190
|
var _b;
|
|
7191
7191
|
(_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
7192
7192
|
for (const f of list)
|
|
7193
|
-
ajv.addFormat(f,
|
|
7193
|
+
ajv.addFormat(f, fs11[f]);
|
|
7194
7194
|
}
|
|
7195
7195
|
module.exports = exports = formatsPlugin;
|
|
7196
7196
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -7572,8 +7572,8 @@ function getErrorMap() {
|
|
|
7572
7572
|
|
|
7573
7573
|
// node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/parseUtil.js
|
|
7574
7574
|
var makeIssue = (params) => {
|
|
7575
|
-
const { data, path:
|
|
7576
|
-
const fullPath = [...
|
|
7575
|
+
const { data, path: path11, errorMaps, issueData } = params;
|
|
7576
|
+
const fullPath = [...path11, ...issueData.path || []];
|
|
7577
7577
|
const fullIssue = {
|
|
7578
7578
|
...issueData,
|
|
7579
7579
|
path: fullPath
|
|
@@ -7688,11 +7688,11 @@ var errorUtil;
|
|
|
7688
7688
|
|
|
7689
7689
|
// node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/types.js
|
|
7690
7690
|
var ParseInputLazyPath = class {
|
|
7691
|
-
constructor(parent, value,
|
|
7691
|
+
constructor(parent, value, path11, key) {
|
|
7692
7692
|
this._cachedPath = [];
|
|
7693
7693
|
this.parent = parent;
|
|
7694
7694
|
this.data = value;
|
|
7695
|
-
this._path =
|
|
7695
|
+
this._path = path11;
|
|
7696
7696
|
this._key = key;
|
|
7697
7697
|
}
|
|
7698
7698
|
get path() {
|
|
@@ -11580,10 +11580,10 @@ function mergeDefs(...defs) {
|
|
|
11580
11580
|
function cloneDef(schema) {
|
|
11581
11581
|
return mergeDefs(schema._zod.def);
|
|
11582
11582
|
}
|
|
11583
|
-
function getElementAtPath(obj,
|
|
11584
|
-
if (!
|
|
11583
|
+
function getElementAtPath(obj, path11) {
|
|
11584
|
+
if (!path11)
|
|
11585
11585
|
return obj;
|
|
11586
|
-
return
|
|
11586
|
+
return path11.reduce((acc, key) => acc?.[key], obj);
|
|
11587
11587
|
}
|
|
11588
11588
|
function promiseAllObject(promisesObj) {
|
|
11589
11589
|
const keys = Object.keys(promisesObj);
|
|
@@ -11995,11 +11995,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
11995
11995
|
}
|
|
11996
11996
|
return false;
|
|
11997
11997
|
}
|
|
11998
|
-
function prefixIssues(
|
|
11998
|
+
function prefixIssues(path11, issues) {
|
|
11999
11999
|
return issues.map((iss) => {
|
|
12000
12000
|
var _a3;
|
|
12001
12001
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
12002
|
-
iss.path.unshift(
|
|
12002
|
+
iss.path.unshift(path11);
|
|
12003
12003
|
return iss;
|
|
12004
12004
|
});
|
|
12005
12005
|
}
|
|
@@ -12432,16 +12432,16 @@ function flattenError(error61, mapper = (issue2) => issue2.message) {
|
|
|
12432
12432
|
}
|
|
12433
12433
|
function formatError(error61, mapper = (issue2) => issue2.message) {
|
|
12434
12434
|
const fieldErrors = { _errors: [] };
|
|
12435
|
-
const processError = (error62,
|
|
12435
|
+
const processError = (error62, path11 = []) => {
|
|
12436
12436
|
for (const issue2 of error62.issues) {
|
|
12437
12437
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
12438
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
12438
|
+
issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
|
|
12439
12439
|
} else if (issue2.code === "invalid_key") {
|
|
12440
|
-
processError({ issues: issue2.issues }, [...
|
|
12440
|
+
processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
|
|
12441
12441
|
} else if (issue2.code === "invalid_element") {
|
|
12442
|
-
processError({ issues: issue2.issues }, [...
|
|
12442
|
+
processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
|
|
12443
12443
|
} else {
|
|
12444
|
-
const fullpath = [...
|
|
12444
|
+
const fullpath = [...path11, ...issue2.path];
|
|
12445
12445
|
if (fullpath.length === 0) {
|
|
12446
12446
|
fieldErrors._errors.push(mapper(issue2));
|
|
12447
12447
|
} else {
|
|
@@ -12480,17 +12480,17 @@ function formatError(error61, mapper = (issue2) => issue2.message) {
|
|
|
12480
12480
|
}
|
|
12481
12481
|
function treeifyError(error61, mapper = (issue2) => issue2.message) {
|
|
12482
12482
|
const result = { errors: [] };
|
|
12483
|
-
const processError = (error62,
|
|
12483
|
+
const processError = (error62, path11 = []) => {
|
|
12484
12484
|
var _a3;
|
|
12485
12485
|
for (const issue2 of error62.issues) {
|
|
12486
12486
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
12487
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
12487
|
+
issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
|
|
12488
12488
|
} else if (issue2.code === "invalid_key") {
|
|
12489
|
-
processError({ issues: issue2.issues }, [...
|
|
12489
|
+
processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
|
|
12490
12490
|
} else if (issue2.code === "invalid_element") {
|
|
12491
|
-
processError({ issues: issue2.issues }, [...
|
|
12491
|
+
processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
|
|
12492
12492
|
} else {
|
|
12493
|
-
const fullpath = [...
|
|
12493
|
+
const fullpath = [...path11, ...issue2.path];
|
|
12494
12494
|
if (fullpath.length === 0) {
|
|
12495
12495
|
result.errors.push(mapper(issue2));
|
|
12496
12496
|
continue;
|
|
@@ -12529,8 +12529,8 @@ function treeifyError(error61, mapper = (issue2) => issue2.message) {
|
|
|
12529
12529
|
}
|
|
12530
12530
|
function toDotPath(_path) {
|
|
12531
12531
|
const segs = [];
|
|
12532
|
-
const
|
|
12533
|
-
for (const seg of
|
|
12532
|
+
const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
12533
|
+
for (const seg of path11) {
|
|
12534
12534
|
if (typeof seg === "number")
|
|
12535
12535
|
segs.push(`[${seg}]`);
|
|
12536
12536
|
else if (typeof seg === "symbol")
|
|
@@ -27511,11 +27511,11 @@ function normalizeObjectSchema(schema) {
|
|
|
27511
27511
|
}
|
|
27512
27512
|
return void 0;
|
|
27513
27513
|
}
|
|
27514
|
-
function getDotPath(
|
|
27515
|
-
if (
|
|
27514
|
+
function getDotPath(path11) {
|
|
27515
|
+
if (path11.length === 0) {
|
|
27516
27516
|
return "object root";
|
|
27517
27517
|
}
|
|
27518
|
-
return
|
|
27518
|
+
return path11.reduce((acc, seg, index) => {
|
|
27519
27519
|
if (index === 0) {
|
|
27520
27520
|
return String(seg);
|
|
27521
27521
|
}
|
|
@@ -29678,13 +29678,13 @@ function resolveRef(ref, ctx) {
|
|
|
29678
29678
|
if (!ref.startsWith("#")) {
|
|
29679
29679
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
29680
29680
|
}
|
|
29681
|
-
const
|
|
29682
|
-
if (
|
|
29681
|
+
const path11 = ref.slice(1).split("/").filter(Boolean);
|
|
29682
|
+
if (path11.length === 0) {
|
|
29683
29683
|
return ctx.rootSchema;
|
|
29684
29684
|
}
|
|
29685
29685
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
29686
|
-
if (
|
|
29687
|
-
const key =
|
|
29686
|
+
if (path11[0] === defsKey) {
|
|
29687
|
+
const key = path11[1] === void 0 ? void 0 : decodeJSONPointerSegment(path11[1]);
|
|
29688
29688
|
if (!key || !ctx.defs[key]) {
|
|
29689
29689
|
throw new Error(`Reference not found: ${ref}`);
|
|
29690
29690
|
}
|
|
@@ -35962,29 +35962,33 @@ function appRoot(cfg, name) {
|
|
|
35962
35962
|
}
|
|
35963
35963
|
|
|
35964
35964
|
// src/instructions.ts
|
|
35965
|
-
var INSTRUCTIONS = `You are helping a
|
|
35965
|
+
var INSTRUCTIONS = `You are helping a person at Kigo build small internal apps with Sindri. Call them the builder: anyone in the company (product, operations, CX, finance, a developer). Assume they are not technical unless they show otherwise: they never see code, git, terminals or ports. You write the code in the app folder on their computer; the sindri tools install, preview, publish and keep versions.
|
|
35966
35966
|
|
|
35967
|
-
Language and tone: always Spanish, "t\xFA", short sentences. Say "app" (feminine: "la app", "tu app"), never "herramienta", "proyecto", "repo", "deploy" or "build". Never paste file paths, commands, logs or stack traces; summarize in one line what happened and what you will do. The \`summary\` field of every tool result is written for the
|
|
35967
|
+
Language and tone: always Spanish, "t\xFA", short sentences. Say "app" (feminine: "la app", "tu app"), never "herramienta", "proyecto", "repo", "deploy" or "build". Never paste file paths, commands, logs or stack traces; summarize in one line what happened and what you will do. The \`summary\` field of every tool result is written for the builder: repeat it as is or paraphrase it.
|
|
35968
35968
|
|
|
35969
|
-
First contact: the first time the
|
|
35969
|
+
First contact: the first time the builder speaks in a conversation, or whenever they ask what Sindri is or can do, call \`help\` and relay its \`summary\` (that is the introduction: what Sindri is, what they can ask for, the limits, the link to Mi espacio), then ask what they want to build. Do not repeat it later in the same conversation. When the builder asks where to see their apps or their versions, give them the Mi espacio link (\`space_url\`, https://sindri.kigo.dev).
|
|
35970
35970
|
|
|
35971
35971
|
Flow:
|
|
35972
|
-
1. First time on a machine: run \`setup\` (installs Node and pnpm without admin rights) and \`login\` (opens the browser on Kigo's sign-in page; the
|
|
35973
|
-
2. New app: ask at most one question if the request is vague (for whom, what it must show), then \`create_app\` with a short lowercase name (it becomes the address https://<name>.sindri.kigo.dev) and a one-sentence description in Spanish. Then edit the files in the folder the tool returns to build what the
|
|
35974
|
-
3. Show before shipping: run \`preview\` and tell the
|
|
35975
|
-
4. Ship: \`publish\` with a message in the
|
|
35976
|
-
5. Later: \`status\` before working on an existing app (tells you whether the local folder is behind or has unpublished edits); \`versions\` and \`rollback\` when the
|
|
35977
|
-
6. Template updates: when the
|
|
35978
|
-
7. Renaming: an app has an address (https://<name>.sindri.kigo.dev) and a visible name (the title people see). When the
|
|
35979
|
-
8. Deleting: \`delete_app\` is final in Sindri (address, versions, database). Before calling it, ask the
|
|
35972
|
+
1. First time on a machine: run \`setup\` (installs Node and pnpm without admin rights) and \`login\` (opens the browser on Kigo's sign-in page; the builder enters with their @cargomovil.com Google account and the browser comes back on its own with "Listo, ya entraste a Sindri"; tell them to return to Claude). \`login\` waits up to five minutes. If any tool answers \`needs_setup\` or \`needs_login\`, run that tool and retry once.
|
|
35973
|
+
2. New app: ask at most one question if the request is vague (for whom, what it must show), then \`create_app\` with a short lowercase name (it becomes the address https://<name>.sindri.kigo.dev) and a one-sentence description in Spanish. Then edit the files in the folder the tool returns to build what the builder asked. The template is Next.js + Drizzle; read its CLAUDE.md first; keep the UI in Spanish, "t\xFA", with the Kigo components it ships.
|
|
35974
|
+
3. Show before shipping: run \`preview\` and tell the builder to look at the local address; ask what to change. Edits reload live.
|
|
35975
|
+
4. Ship: \`publish\` with a message in the builder's words ("Filtro por fecha"). Give the URL and say the team enters with their Google de Kigo. If publish fails, the published version did not change: say what broke in one line, fix it, publish again. The published app has its own database: publish runs the migrations and, when this version changed drizzle/ or src/db/seed.ts, loads the test data again; rows the builder typed into the local preview never travel. Keep test data in the seed, not in the UI.
|
|
35976
|
+
5. Later: \`status\` before working on an existing app (tells you whether the local folder is behind or has unpublished edits); \`versions\` and \`rollback\` when the builder wants "como estaba ayer"; \`share\` for the Slack line.
|
|
35977
|
+
6. Template updates: when the builder says "actualiza la plantilla" or Sindri announces a template fix, run \`update_template\`. It replaces only the files the app never customised, publishes \xABPlantilla actualizada\xBB and brings the local folder to it; the files you had changed are listed in \`skipped\` so you can apply the same change there by hand if it matters. It refuses while there are unpublished local edits: publish them first (or tell the builder what would be lost) and retry.
|
|
35978
|
+
7. Renaming: an app has an address (https://<name>.sindri.kigo.dev) and a visible name (the title people see). When the builder says "c\xE1mbiale el nombre" and it is not obvious which one they mean, ask: "\xBFla direcci\xF3n o el nombre que se ve?". \`rename_app\` with \`new_name\` changes the address (Sindri renames it, the local folder follows, the old link stops working: tell the builder to share the new one); it refuses while there are unpublished edits, so publish first. \`rename_app\` with \`display_name\` changes only the visible name and publishes it as \xABAhora se llama \xAB<nombre>\xBB\xBB. Both in one call is fine.
|
|
35979
|
+
8. Deleting: \`delete_app\` is final in Sindri (address, versions, database). Before calling it, ask the builder to type the app's name to confirm and pass exactly what they wrote as \`confirm\`; never fill it in yourself, never guess it, never call it because you inferred they want it. The local files are moved to the Sindri papelera on their computer, not deleted; say so.
|
|
35980
35980
|
|
|
35981
|
-
|
|
35981
|
+
Secrets: when the app needs a credential (an API key, a token, a password for another service), never ask the builder to paste it in the chat and never write it in code, seeds or comments. Call \`set_secret\` with the variable name (UPPER_SNAKE_CASE, e.g. LINEAR_API_KEY) and a one-line \`hint\` in Spanish saying what to paste; tell the builder "se abri\xF3 una p\xE1gina en tu navegador para pegarlo" and wait: the tool returns when they saved it (or after five minutes). The value is stored in the app's .env.local on their computer and in Sindri; the running preview restarts so the code sees it. In code read it as \`process.env.NAME\` (server side only: route handlers, server components, server actions) and handle the missing case with a clear Spanish message. \`list_secrets\` tells you which names exist on each side (never values); \`delete_secret\` removes one after the builder agrees. Never read .env.local (or any .env* file) and never repeat its contents to the builder, not even partially.
|
|
35982
35982
|
|
|
35983
|
-
|
|
35983
|
+
Tasks: when something must happen on its own on a schedule (sync data from another service, send a summary, recompute a table), declare it in kigo-app.yaml with ONE line \`schedules: [{"name":"sync-linear","path":"/api/tareas/sync-linear","every":"30m"}]\` (a JSON list: \`name\` lowercase with hyphens and unique, \`path\` under /api/ without query, \`every\` one of 5m, 10m, 15m, 30m, 1h, 2h, 6h, 12h, 1d; at most 5 tasks). The route at that path must be built with the template's \`taskHandler(name, fn)\` from src/lib/tasks.ts (see the app's CLAUDE.md and the design guide topic tasks-and-secrets): it only accepts the platform's signed calls, so never call it from the UI. Test it with \`run_task\` locally (preview running) before publishing, then \`run_task\` with \`where: "sindri"\` once published; \`status\` shows the last run of each task. Remind the builder that in Sindri tasks run only in business hours (Monday to Friday, 9:05 am to midnight, Mexico City), never on weekends or at night.
|
|
35984
|
+
|
|
35985
|
+
Design guide: before creating or changing any page, component, layout, style or copy, call \`design_guide\` for the app, first without \`topic\` (the overview and the list of topics), then with the topic that matches what you are building (admin, landing, mini-app, mobile; tokens, writing and checklist when relevant). Follow it; it is for you, never paste its content to the builder. If it answers \`no_design_guide\`, run \`update_template\` once and call \`design_guide\` again.
|
|
35986
|
+
|
|
35987
|
+
Rules: synthetic data only (never paste real emails, plates or phone numbers into seeds); one author per app (the builder); never touch files outside the app folder; never read .env.local back to the builder; do not edit middleware.ts, next.config.ts or package.json scripts (publish will refuse them); apps work Monday to Friday 9:05 am to midnight Mexico City time and the link says so outside those hours. When something fails, explain in one line what to do next; never blame the builder.`;
|
|
35984
35988
|
|
|
35985
35989
|
// src/tools.ts
|
|
35986
|
-
import
|
|
35987
|
-
import
|
|
35990
|
+
import fs10 from "node:fs/promises";
|
|
35991
|
+
import path10 from "node:path";
|
|
35988
35992
|
|
|
35989
35993
|
// src/api.ts
|
|
35990
35994
|
import fs3 from "node:fs/promises";
|
|
@@ -36367,6 +36371,20 @@ var ApiClient = class {
|
|
|
36367
36371
|
share(name) {
|
|
36368
36372
|
return this.json("GET", `/v1/apps/${enc(name)}/share`);
|
|
36369
36373
|
}
|
|
36374
|
+
listSecrets(name) {
|
|
36375
|
+
return this.json("GET", `/v1/apps/${enc(name)}/secrets`).then((r) => r.names ?? []);
|
|
36376
|
+
}
|
|
36377
|
+
/** Stores one secret in the app's workspace; `restarted` says whether the app was restarted to pick it up. */
|
|
36378
|
+
putSecret(name, key, value) {
|
|
36379
|
+
return this.json("PUT", `/v1/apps/${enc(name)}/secrets/${enc(key)}`, { value }, 12e4);
|
|
36380
|
+
}
|
|
36381
|
+
deleteSecret(name, key) {
|
|
36382
|
+
return this.json("DELETE", `/v1/apps/${enc(name)}/secrets/${enc(key)}`, void 0, 12e4);
|
|
36383
|
+
}
|
|
36384
|
+
/** Runs a declared task now in the workspace and waits for it (the router caps the run). */
|
|
36385
|
+
runTask(name, task) {
|
|
36386
|
+
return this.json("POST", `/v1/apps/${enc(name)}/tasks/${enc(task)}/run`, {}, 32e4);
|
|
36387
|
+
}
|
|
36370
36388
|
/** Downloads an archive (`archive_url` from create, or /archive of a version) to a temp file. */
|
|
36371
36389
|
async download(urlOrPath, version2) {
|
|
36372
36390
|
const u = new URL(urlOrPath.startsWith("http") ? urlOrPath : `${this.cfg.apiUrl}${urlOrPath}`);
|
|
@@ -36423,22 +36441,126 @@ function enc(name) {
|
|
|
36423
36441
|
return encodeURIComponent(name);
|
|
36424
36442
|
}
|
|
36425
36443
|
|
|
36426
|
-
// src/
|
|
36444
|
+
// src/envfile.ts
|
|
36445
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
36427
36446
|
import fs4 from "node:fs/promises";
|
|
36428
36447
|
import path4 from "node:path";
|
|
36448
|
+
var ENV_FILE = ".env.local";
|
|
36449
|
+
var PLATFORM_KEYS = ["KIGO_ENV", "KIGO_APP_NAME", "DATABASE_URL", "PGLITE_DATA_DIR", "SINDRI_TASK_TOKEN"];
|
|
36450
|
+
var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]{1,63}$/;
|
|
36451
|
+
function validSecretName(name) {
|
|
36452
|
+
return typeof name === "string" && SECRET_NAME_RE.test(name);
|
|
36453
|
+
}
|
|
36454
|
+
function reservedKey(name) {
|
|
36455
|
+
return PLATFORM_KEYS.includes(name);
|
|
36456
|
+
}
|
|
36457
|
+
var LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
36458
|
+
function envKeys(text) {
|
|
36459
|
+
const out = [];
|
|
36460
|
+
for (const line of text.split("\n")) {
|
|
36461
|
+
const m = LINE_RE.exec(line);
|
|
36462
|
+
if (m?.[1] && !out.includes(m[1])) out.push(m[1]);
|
|
36463
|
+
}
|
|
36464
|
+
return out;
|
|
36465
|
+
}
|
|
36466
|
+
function envValue(text, key) {
|
|
36467
|
+
for (const line of text.split("\n")) {
|
|
36468
|
+
const m = LINE_RE.exec(line);
|
|
36469
|
+
if (!m || m[1] !== key) continue;
|
|
36470
|
+
const raw = (m[2] ?? "").trim();
|
|
36471
|
+
const q = raw[0];
|
|
36472
|
+
if ((q === '"' || q === "'" || q === "`") && raw.length >= 2) {
|
|
36473
|
+
const end = raw.indexOf(q, 1);
|
|
36474
|
+
const inner = end > 0 ? raw.slice(1, end) : raw.slice(1);
|
|
36475
|
+
return q === '"' ? inner.replace(/\\n/g, "\n") : inner;
|
|
36476
|
+
}
|
|
36477
|
+
return raw.replace(/\s+#.*$/, "").trim();
|
|
36478
|
+
}
|
|
36479
|
+
return void 0;
|
|
36480
|
+
}
|
|
36481
|
+
function envLine(key, value) {
|
|
36482
|
+
return `${key}=${quoteEnvValue(value)}`;
|
|
36483
|
+
}
|
|
36484
|
+
function quoteEnvValue(value) {
|
|
36485
|
+
if (value !== "" && /^[A-Za-z0-9_.\/:@+=,\-]+$/.test(value)) return value;
|
|
36486
|
+
const order = value.includes("$") ? ["'", '"', "`"] : ['"', "'", "`"];
|
|
36487
|
+
for (const q of order) {
|
|
36488
|
+
if (value.includes(q)) continue;
|
|
36489
|
+
if (q === '"') return `"${value.replace(/\r?\n/g, "\\n")}"`;
|
|
36490
|
+
if (value.includes("\n")) continue;
|
|
36491
|
+
return `${q}${value}${q}`;
|
|
36492
|
+
}
|
|
36493
|
+
throw new Error("value cannot be quoted for .env.local");
|
|
36494
|
+
}
|
|
36495
|
+
function upsertEnv(text, key, value) {
|
|
36496
|
+
const line = envLine(key, value);
|
|
36497
|
+
const lines = text.split("\n");
|
|
36498
|
+
const hadTrailing = lines.at(-1) === "";
|
|
36499
|
+
if (hadTrailing) lines.pop();
|
|
36500
|
+
let done = false;
|
|
36501
|
+
const out = [];
|
|
36502
|
+
for (const l of lines) {
|
|
36503
|
+
if (LINE_RE.exec(l)?.[1] === key) {
|
|
36504
|
+
if (!done) out.push(line);
|
|
36505
|
+
done = true;
|
|
36506
|
+
continue;
|
|
36507
|
+
}
|
|
36508
|
+
out.push(l);
|
|
36509
|
+
}
|
|
36510
|
+
if (!done) out.push(line);
|
|
36511
|
+
return `${out.join("\n")}
|
|
36512
|
+
`;
|
|
36513
|
+
}
|
|
36514
|
+
function removeEnv(text, key) {
|
|
36515
|
+
const lines = text.split("\n");
|
|
36516
|
+
const hadTrailing = lines.at(-1) === "";
|
|
36517
|
+
if (hadTrailing) lines.pop();
|
|
36518
|
+
const out = lines.filter((l) => LINE_RE.exec(l)?.[1] !== key);
|
|
36519
|
+
return out.length === 0 ? "" : `${out.join("\n")}
|
|
36520
|
+
`;
|
|
36521
|
+
}
|
|
36522
|
+
function newTaskToken() {
|
|
36523
|
+
return randomBytes2(32).toString("hex");
|
|
36524
|
+
}
|
|
36525
|
+
function envFile(appDir) {
|
|
36526
|
+
return path4.join(appDir, ENV_FILE);
|
|
36527
|
+
}
|
|
36528
|
+
async function readEnvFile(appDir) {
|
|
36529
|
+
return fs4.readFile(envFile(appDir), "utf8").catch(() => "");
|
|
36530
|
+
}
|
|
36531
|
+
async function writeEnvFile(appDir, text) {
|
|
36532
|
+
await fs4.writeFile(envFile(appDir), text, { mode: 384 });
|
|
36533
|
+
}
|
|
36534
|
+
async function ensureLocalEnv(appDir, app) {
|
|
36535
|
+
const before = await readEnvFile(appDir);
|
|
36536
|
+
let text = before;
|
|
36537
|
+
if (envValue(text, "KIGO_ENV") === void 0) text = upsertEnv(text, "KIGO_ENV", "local");
|
|
36538
|
+
if (envValue(text, "KIGO_APP_NAME") === void 0) text = upsertEnv(text, "KIGO_APP_NAME", app);
|
|
36539
|
+
const createdToken = !envValue(text, "SINDRI_TASK_TOKEN");
|
|
36540
|
+
if (createdToken) text = upsertEnv(text, "SINDRI_TASK_TOKEN", newTaskToken());
|
|
36541
|
+
if (text !== before) await writeEnvFile(appDir, text);
|
|
36542
|
+
return { created_token: createdToken };
|
|
36543
|
+
}
|
|
36544
|
+
function secretKeys(text) {
|
|
36545
|
+
return envKeys(text).filter((k) => !reservedKey(k));
|
|
36546
|
+
}
|
|
36547
|
+
|
|
36548
|
+
// src/layout.ts
|
|
36549
|
+
import fs5 from "node:fs/promises";
|
|
36550
|
+
import path5 from "node:path";
|
|
36429
36551
|
async function appPaths(cfg, name, ownerHint) {
|
|
36430
36552
|
const root = appRoot(cfg, name);
|
|
36431
36553
|
const discovered = await ownerFromApps(root, name);
|
|
36432
36554
|
const owner = discovered ?? ownerHint ?? await cachedOwner(cfg) ?? "";
|
|
36433
|
-
return { root, appDir: owner ?
|
|
36555
|
+
return { root, appDir: owner ? path5.join(root, "apps", owner, name) : path5.join(root, "apps", "_", name), owner };
|
|
36434
36556
|
}
|
|
36435
36557
|
async function ownerFromApps(root, name) {
|
|
36436
36558
|
try {
|
|
36437
|
-
const owners = (await
|
|
36559
|
+
const owners = (await fs5.readdir(path5.join(root, "apps"), { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
36438
36560
|
const withApp = [];
|
|
36439
36561
|
for (const o of owners) {
|
|
36440
36562
|
try {
|
|
36441
|
-
await
|
|
36563
|
+
await fs5.access(path5.join(root, "apps", o.name, name, "package.json"));
|
|
36442
36564
|
withApp.push(o.name);
|
|
36443
36565
|
} catch {
|
|
36444
36566
|
}
|
|
@@ -36451,15 +36573,15 @@ async function ownerFromApps(root, name) {
|
|
|
36451
36573
|
}
|
|
36452
36574
|
}
|
|
36453
36575
|
function meFile(cfg) {
|
|
36454
|
-
return
|
|
36576
|
+
return path5.join(cfg.stateDir, "me.json");
|
|
36455
36577
|
}
|
|
36456
36578
|
async function rememberMe(cfg, me) {
|
|
36457
|
-
await
|
|
36458
|
-
await
|
|
36579
|
+
await fs5.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
|
|
36580
|
+
await fs5.writeFile(meFile(cfg), JSON.stringify(me), { mode: 384 });
|
|
36459
36581
|
}
|
|
36460
36582
|
async function cachedOwner(cfg) {
|
|
36461
36583
|
try {
|
|
36462
|
-
const me = JSON.parse(await
|
|
36584
|
+
const me = JSON.parse(await fs5.readFile(meFile(cfg), "utf8"));
|
|
36463
36585
|
return me.owner || null;
|
|
36464
36586
|
} catch {
|
|
36465
36587
|
return null;
|
|
@@ -36467,63 +36589,79 @@ async function cachedOwner(cfg) {
|
|
|
36467
36589
|
}
|
|
36468
36590
|
async function appPresent(p) {
|
|
36469
36591
|
try {
|
|
36470
|
-
await
|
|
36592
|
+
await fs5.access(path5.join(p.appDir, "package.json"));
|
|
36471
36593
|
return true;
|
|
36472
36594
|
} catch {
|
|
36473
36595
|
return false;
|
|
36474
36596
|
}
|
|
36475
36597
|
}
|
|
36476
|
-
var DESIGN_GUIDE_REL =
|
|
36598
|
+
var DESIGN_GUIDE_REL = path5.join(".claude", "skills", "kigo-sindri-design");
|
|
36477
36599
|
async function readDesignGuide(p, topic) {
|
|
36478
|
-
const dir =
|
|
36600
|
+
const dir = path5.join(p.appDir, DESIGN_GUIDE_REL);
|
|
36479
36601
|
let skill;
|
|
36480
36602
|
try {
|
|
36481
|
-
skill = await
|
|
36603
|
+
skill = await fs5.readFile(path5.join(dir, "SKILL.md"), "utf8");
|
|
36482
36604
|
} catch {
|
|
36483
36605
|
return null;
|
|
36484
36606
|
}
|
|
36485
36607
|
const topics = await designTopics(dir);
|
|
36486
36608
|
if (topic === void 0) return { content: skill, topics };
|
|
36487
36609
|
if (!topics.includes(topic)) return { unknown: true, topics };
|
|
36488
|
-
return { content: await
|
|
36610
|
+
return { content: await fs5.readFile(path5.join(dir, "references", `${topic}.md`), "utf8"), topics };
|
|
36489
36611
|
}
|
|
36490
36612
|
async function designTopics(dir) {
|
|
36491
36613
|
try {
|
|
36492
|
-
const entries = await
|
|
36614
|
+
const entries = await fs5.readdir(path5.join(dir, "references"), { withFileTypes: true });
|
|
36493
36615
|
return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith(".")).map((e) => e.name.slice(0, -".md".length)).sort();
|
|
36494
36616
|
} catch {
|
|
36495
36617
|
return [];
|
|
36496
36618
|
}
|
|
36497
36619
|
}
|
|
36498
36620
|
async function linkDesignGuide(p) {
|
|
36499
|
-
const target =
|
|
36621
|
+
const target = path5.join(p.appDir, DESIGN_GUIDE_REL);
|
|
36500
36622
|
try {
|
|
36501
|
-
if (!(await
|
|
36623
|
+
if (!(await fs5.stat(target)).isDirectory()) return false;
|
|
36502
36624
|
} catch {
|
|
36503
36625
|
return false;
|
|
36504
36626
|
}
|
|
36505
|
-
const link =
|
|
36506
|
-
const rel =
|
|
36507
|
-
for (const parent of [
|
|
36508
|
-
const st = await
|
|
36627
|
+
const link = path5.join(p.root, DESIGN_GUIDE_REL);
|
|
36628
|
+
const rel = path5.relative(path5.dirname(link), target);
|
|
36629
|
+
for (const parent of [path5.join(p.root, ".claude"), path5.dirname(link)]) {
|
|
36630
|
+
const st = await fs5.lstat(parent).catch(() => null);
|
|
36509
36631
|
if (st && !st.isDirectory()) return false;
|
|
36510
36632
|
}
|
|
36511
|
-
const existing = await
|
|
36633
|
+
const existing = await fs5.lstat(link).catch(() => null);
|
|
36512
36634
|
if (existing) {
|
|
36513
36635
|
if (!existing.isSymbolicLink()) return false;
|
|
36514
|
-
if (await
|
|
36515
|
-
await
|
|
36636
|
+
if (await fs5.readlink(link) === rel) return true;
|
|
36637
|
+
await fs5.unlink(link);
|
|
36516
36638
|
}
|
|
36517
|
-
await
|
|
36518
|
-
await
|
|
36639
|
+
await fs5.mkdir(path5.dirname(link), { recursive: true });
|
|
36640
|
+
await fs5.symlink(rel, link, "dir");
|
|
36519
36641
|
return true;
|
|
36520
36642
|
}
|
|
36521
36643
|
|
|
36522
36644
|
// src/manifest.ts
|
|
36523
|
-
import
|
|
36524
|
-
import
|
|
36645
|
+
import fs6 from "node:fs/promises";
|
|
36646
|
+
import path6 from "node:path";
|
|
36525
36647
|
var MANIFEST_FILE = "kigo-app.yaml";
|
|
36526
36648
|
var KEY_RE = /^([a-z_]+):\s*(.*)$/;
|
|
36649
|
+
function manifestValue(text, key) {
|
|
36650
|
+
for (const line of text.split("\n")) {
|
|
36651
|
+
const m = KEY_RE.exec(line);
|
|
36652
|
+
if (!m || m[1] !== key) continue;
|
|
36653
|
+
const raw = (m[2] ?? "").trim();
|
|
36654
|
+
if (raw.startsWith('"')) {
|
|
36655
|
+
try {
|
|
36656
|
+
return JSON.parse(raw);
|
|
36657
|
+
} catch {
|
|
36658
|
+
return raw.slice(1, -1);
|
|
36659
|
+
}
|
|
36660
|
+
}
|
|
36661
|
+
return raw;
|
|
36662
|
+
}
|
|
36663
|
+
return void 0;
|
|
36664
|
+
}
|
|
36527
36665
|
function setManifestValue(text, key, value) {
|
|
36528
36666
|
const line = `${key}: ${JSON.stringify(value)}`;
|
|
36529
36667
|
const lines = text.split("\n");
|
|
@@ -36537,29 +36675,29 @@ function setManifestValue(text, key, value) {
|
|
|
36537
36675
|
return lines.join("\n");
|
|
36538
36676
|
}
|
|
36539
36677
|
async function readManifest(appDir) {
|
|
36540
|
-
return
|
|
36678
|
+
return fs6.readFile(path6.join(appDir, MANIFEST_FILE), "utf8").catch(() => "");
|
|
36541
36679
|
}
|
|
36542
36680
|
async function writeManifestValue(appDir, key, value) {
|
|
36543
36681
|
const text = await readManifest(appDir);
|
|
36544
|
-
await
|
|
36682
|
+
await fs6.writeFile(path6.join(appDir, MANIFEST_FILE), setManifestValue(text, key, value));
|
|
36545
36683
|
}
|
|
36546
36684
|
|
|
36547
36685
|
// src/preview.ts
|
|
36548
|
-
import
|
|
36686
|
+
import fs8 from "node:fs/promises";
|
|
36549
36687
|
import net from "node:net";
|
|
36550
|
-
import
|
|
36688
|
+
import path8 from "node:path";
|
|
36551
36689
|
|
|
36552
36690
|
// src/toolchain.ts
|
|
36553
|
-
import
|
|
36554
|
-
import
|
|
36691
|
+
import fs7 from "node:fs/promises";
|
|
36692
|
+
import path7 from "node:path";
|
|
36555
36693
|
var MIN_NODE = 20;
|
|
36556
36694
|
var PNPM_ARGS = ["--config.verify-deps-before-run=false"];
|
|
36557
36695
|
async function resolveToolchain(cfg, env = process.env) {
|
|
36558
36696
|
const fnmBin = await fnmDefaultBin(cfg);
|
|
36559
36697
|
if (fnmBin) {
|
|
36560
|
-
const childEnv2 = { ...env, PATH: `${fnmBin}${
|
|
36561
|
-
const node2 =
|
|
36562
|
-
const pnpm =
|
|
36698
|
+
const childEnv2 = { ...env, PATH: `${fnmBin}${path7.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
|
|
36699
|
+
const node2 = path7.join(fnmBin, "node");
|
|
36700
|
+
const pnpm = path7.join(fnmBin, "pnpm");
|
|
36563
36701
|
if (await versionOf(node2, childEnv2) >= MIN_NODE && await ok(pnpm, childEnv2)) {
|
|
36564
36702
|
return { nodeBin: fnmBin, node: node2, pnpm, env: childEnv2, source: "fnm" };
|
|
36565
36703
|
}
|
|
@@ -36567,20 +36705,20 @@ async function resolveToolchain(cfg, env = process.env) {
|
|
|
36567
36705
|
const childEnv = { ...env, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
|
|
36568
36706
|
if (await versionOf("node", childEnv) >= MIN_NODE && await ok("pnpm", childEnv)) {
|
|
36569
36707
|
const which = await run("node", ["-e", "process.stdout.write(process.execPath)"], { env: childEnv, timeoutMs: 1e4 });
|
|
36570
|
-
return { nodeBin:
|
|
36708
|
+
return { nodeBin: path7.dirname(which.stdout || "node"), node: "node", pnpm: "pnpm", env: childEnv, source: "path" };
|
|
36571
36709
|
}
|
|
36572
36710
|
return null;
|
|
36573
36711
|
}
|
|
36574
36712
|
async function fnmDefaultBin(cfg) {
|
|
36575
|
-
const def =
|
|
36576
|
-
if (await exists(
|
|
36577
|
-
const versions =
|
|
36713
|
+
const def = path7.join(cfg.fnmDir, "aliases", "default", "bin");
|
|
36714
|
+
if (await exists(path7.join(def, "node"))) return def;
|
|
36715
|
+
const versions = path7.join(cfg.fnmDir, "node-versions");
|
|
36578
36716
|
try {
|
|
36579
|
-
const names = (await
|
|
36717
|
+
const names = (await fs7.readdir(versions)).filter((n) => n.startsWith("v")).sort(compareVersions);
|
|
36580
36718
|
const newest = names[names.length - 1];
|
|
36581
36719
|
if (!newest) return null;
|
|
36582
|
-
const bin =
|
|
36583
|
-
return await exists(
|
|
36720
|
+
const bin = path7.join(versions, newest, "installation", "bin");
|
|
36721
|
+
return await exists(path7.join(bin, "node")) ? bin : null;
|
|
36584
36722
|
} catch {
|
|
36585
36723
|
return null;
|
|
36586
36724
|
}
|
|
@@ -36605,7 +36743,7 @@ function compareVersions(a, b) {
|
|
|
36605
36743
|
}
|
|
36606
36744
|
async function exists(p) {
|
|
36607
36745
|
try {
|
|
36608
|
-
await
|
|
36746
|
+
await fs7.access(p);
|
|
36609
36747
|
return true;
|
|
36610
36748
|
} catch {
|
|
36611
36749
|
return false;
|
|
@@ -36623,42 +36761,49 @@ async function setupToolchain(cfg, opts = {}) {
|
|
|
36623
36761
|
}
|
|
36624
36762
|
const node2 = (await run(tc.node, ["--version"], { env: tc.env, timeoutMs: 1e4 })).stdout.trim();
|
|
36625
36763
|
const pnpm = (await run(tc.pnpm, ["--version"], { env: tc.env, timeoutMs: 2e4 })).stdout.trim();
|
|
36626
|
-
await
|
|
36764
|
+
await fs7.mkdir(cfg.home, { recursive: true });
|
|
36627
36765
|
return { installed, node: node2, pnpm, source: tc.source };
|
|
36628
36766
|
}
|
|
36767
|
+
function fnmInstallerArgs(installDir, platform = process.platform) {
|
|
36768
|
+
return ["-s", "--", "--install-dir", installDir, "--skip-shell", ...platform === "darwin" ? ["--force-no-brew"] : []];
|
|
36769
|
+
}
|
|
36770
|
+
function stepFailed(step, r) {
|
|
36771
|
+
const out = [r.stderr.trim(), r.stdout.trim()].filter(Boolean).join("\n");
|
|
36772
|
+
return new Error(`${step} failed (exit ${r.code}${r.timedOut ? ", timeout" : ""}): ${out.slice(-300) || "sin salida"}`);
|
|
36773
|
+
}
|
|
36629
36774
|
async function installFnmLts(cfg, env) {
|
|
36630
|
-
await
|
|
36775
|
+
await fs7.mkdir(cfg.fnmDir, { recursive: true });
|
|
36631
36776
|
const script = await (await fetch("https://fnm.vercel.app/install")).text();
|
|
36632
|
-
const sh = await run("bash",
|
|
36633
|
-
if (sh.code !== 0) throw
|
|
36634
|
-
const fnm =
|
|
36777
|
+
const sh = await run("bash", fnmInstallerArgs(cfg.fnmDir), { env, input: script, timeoutMs: 3e5 });
|
|
36778
|
+
if (sh.code !== 0) throw stepFailed("fnm installer", sh);
|
|
36779
|
+
const fnm = path7.join(cfg.fnmDir, "fnm");
|
|
36635
36780
|
const fnmEnv = { ...env, FNM_DIR: cfg.fnmDir };
|
|
36636
36781
|
const inst = await run(fnm, ["install", "--lts"], { env: fnmEnv, timeoutMs: 6e5 });
|
|
36637
|
-
if (inst.code !== 0) throw
|
|
36782
|
+
if (inst.code !== 0) throw stepFailed("fnm install --lts", inst);
|
|
36638
36783
|
const def = await run(fnm, ["default", "lts-latest"], { env: fnmEnv, timeoutMs: 6e4 });
|
|
36639
|
-
if (def.code !== 0) throw
|
|
36640
|
-
const bin =
|
|
36641
|
-
const childEnv = { ...env, PATH: `${bin}${
|
|
36642
|
-
const enable = await run(
|
|
36643
|
-
if (enable.code !== 0) throw
|
|
36644
|
-
const prep = await run(
|
|
36645
|
-
if (prep.code !== 0) throw
|
|
36784
|
+
if (def.code !== 0) throw stepFailed("fnm default", def);
|
|
36785
|
+
const bin = path7.join(cfg.fnmDir, "aliases", "default", "bin");
|
|
36786
|
+
const childEnv = { ...env, PATH: `${bin}${path7.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
|
|
36787
|
+
const enable = await run(path7.join(bin, "corepack"), ["enable", "--install-directory", bin], { env: childEnv, timeoutMs: 6e4 });
|
|
36788
|
+
if (enable.code !== 0) throw stepFailed("corepack enable", enable);
|
|
36789
|
+
const prep = await run(path7.join(bin, "corepack"), ["prepare", `pnpm@${cfg.pnpmVersion}`, "--activate"], { env: childEnv, timeoutMs: 3e5 });
|
|
36790
|
+
if (prep.code !== 0) throw stepFailed("corepack prepare pnpm", prep);
|
|
36646
36791
|
}
|
|
36647
36792
|
|
|
36648
36793
|
// src/preview.ts
|
|
36649
36794
|
function registryFile(cfg) {
|
|
36650
|
-
return
|
|
36795
|
+
return path8.join(cfg.stateDir, "previews.json");
|
|
36651
36796
|
}
|
|
36652
36797
|
async function readRegistry(cfg) {
|
|
36653
36798
|
try {
|
|
36654
|
-
return JSON.parse(await
|
|
36799
|
+
return JSON.parse(await fs8.readFile(registryFile(cfg), "utf8"));
|
|
36655
36800
|
} catch {
|
|
36656
36801
|
return {};
|
|
36657
36802
|
}
|
|
36658
36803
|
}
|
|
36659
36804
|
async function writeRegistry(cfg, reg) {
|
|
36660
|
-
await
|
|
36661
|
-
await
|
|
36805
|
+
await fs8.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
|
|
36806
|
+
await fs8.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
|
|
36662
36807
|
}
|
|
36663
36808
|
async function freePort(from = 3e3, to = 3099) {
|
|
36664
36809
|
for (let p = from; p <= to; p++) {
|
|
@@ -36695,9 +36840,9 @@ async function startPreview(cfg, tc, app, dir, opts = {}) {
|
|
|
36695
36840
|
const existing = await runningPreview(cfg, app, fetchImpl);
|
|
36696
36841
|
if (existing) return { port: existing.port, url: `http://127.0.0.1:${existing.port}`, reused: true };
|
|
36697
36842
|
const port = await freePort();
|
|
36698
|
-
await
|
|
36699
|
-
const logFile =
|
|
36700
|
-
await
|
|
36843
|
+
await fs8.mkdir(path8.join(dir, ".sindri"), { recursive: true });
|
|
36844
|
+
const logFile = path8.join(dir, ".sindri", "preview.log");
|
|
36845
|
+
await fs8.writeFile(logFile, "");
|
|
36701
36846
|
const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", PORT: String(port), CI: "1", FORCE_COLOR: "0", BROWSER: "none" };
|
|
36702
36847
|
const pid = spawnDetached(tc.pnpm, [...PNPM_ARGS, "dev", "--port", String(port)], { cwd: dir, env, logFile });
|
|
36703
36848
|
const reg = await readRegistry(cfg);
|
|
@@ -36713,7 +36858,7 @@ async function startPreview(cfg, tc, app, dir, opts = {}) {
|
|
|
36713
36858
|
killTree(pid);
|
|
36714
36859
|
delete reg[app];
|
|
36715
36860
|
await writeRegistry(cfg, reg);
|
|
36716
|
-
const log = await
|
|
36861
|
+
const log = await fs8.readFile(logFile, "utf8").catch(() => "");
|
|
36717
36862
|
return { port, url: `http://127.0.0.1:${port}`, reused: false, log: log.split("\n").slice(-15).join("\n") };
|
|
36718
36863
|
}
|
|
36719
36864
|
async function stopPreview(cfg, app) {
|
|
@@ -36730,11 +36875,200 @@ async function openInBrowser(cfg, url2) {
|
|
|
36730
36875
|
await run(cfg.opener, [url2], { timeoutMs: 1e4 });
|
|
36731
36876
|
}
|
|
36732
36877
|
|
|
36878
|
+
// src/schedules.ts
|
|
36879
|
+
var SCHEDULE_NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;
|
|
36880
|
+
var SCHEDULE_EVERY = ["5m", "10m", "15m", "30m", "1h", "2h", "6h", "12h", "1d"];
|
|
36881
|
+
var SCHEDULES_MAX = 5;
|
|
36882
|
+
function readSchedules(manifestText) {
|
|
36883
|
+
const raw = manifestValue(manifestText, "schedules");
|
|
36884
|
+
if (raw === void 0 || raw.trim() === "") return { ok: true, tasks: [] };
|
|
36885
|
+
let parsed;
|
|
36886
|
+
try {
|
|
36887
|
+
parsed = JSON.parse(raw);
|
|
36888
|
+
} catch {
|
|
36889
|
+
return { ok: false, error: "La l\xEDnea schedules de kigo-app.yaml no es una lista JSON v\xE1lida." };
|
|
36890
|
+
}
|
|
36891
|
+
if (!Array.isArray(parsed)) return { ok: false, error: "La l\xEDnea schedules de kigo-app.yaml debe ser una lista JSON." };
|
|
36892
|
+
if (parsed.length > SCHEDULES_MAX) return { ok: false, error: `Una app puede declarar hasta ${SCHEDULES_MAX} tareas.` };
|
|
36893
|
+
const tasks = [];
|
|
36894
|
+
for (const item of parsed) {
|
|
36895
|
+
if (!item || typeof item !== "object") return { ok: false, error: "Cada tarea de schedules debe ser un objeto {name, path, every}." };
|
|
36896
|
+
const { name, path: path11, every } = item;
|
|
36897
|
+
if (typeof name !== "string" || !SCHEDULE_NAME_RE.test(name)) {
|
|
36898
|
+
return { ok: false, error: `El nombre de tarea ${JSON.stringify(name)} no es v\xE1lido: min\xFAsculas, d\xEDgitos y guiones, de 2 a 32 letras.` };
|
|
36899
|
+
}
|
|
36900
|
+
if (tasks.some((t) => t.name === name)) return { ok: false, error: `La tarea \xAB${name}\xBB est\xE1 declarada dos veces.` };
|
|
36901
|
+
if (typeof path11 !== "string" || !path11.startsWith("/api/") || path11.includes("?") || path11.includes("#")) {
|
|
36902
|
+
return { ok: false, error: `La ruta de la tarea \xAB${name}\xBB debe empezar con /api/ y no llevar par\xE1metros.` };
|
|
36903
|
+
}
|
|
36904
|
+
if (typeof every !== "string" || !SCHEDULE_EVERY.includes(every)) {
|
|
36905
|
+
return { ok: false, error: `La frecuencia de la tarea \xAB${name}\xBB debe ser una de: ${SCHEDULE_EVERY.join(", ")}.` };
|
|
36906
|
+
}
|
|
36907
|
+
tasks.push({ name, path: path11, every });
|
|
36908
|
+
}
|
|
36909
|
+
return { ok: true, tasks };
|
|
36910
|
+
}
|
|
36911
|
+
|
|
36912
|
+
// src/secretpage.ts
|
|
36913
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
36914
|
+
import http2 from "node:http";
|
|
36915
|
+
var SECRET_MAX_BYTES = 8 * 1024;
|
|
36916
|
+
var SecretPageBrowserUnavailable = class extends Error {
|
|
36917
|
+
constructor() {
|
|
36918
|
+
super("browser unavailable");
|
|
36919
|
+
}
|
|
36920
|
+
};
|
|
36921
|
+
async function collectSecret(opts) {
|
|
36922
|
+
const nonce = randomBytes3(16).toString("base64url");
|
|
36923
|
+
const server = http2.createServer();
|
|
36924
|
+
await new Promise((resolve, reject) => {
|
|
36925
|
+
server.once("error", reject);
|
|
36926
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
36927
|
+
});
|
|
36928
|
+
const port = server.address().port;
|
|
36929
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
36930
|
+
const route = `/s/${nonce}`;
|
|
36931
|
+
const url2 = `${origin}${route}`;
|
|
36932
|
+
let timer;
|
|
36933
|
+
let settled = null;
|
|
36934
|
+
let used = false;
|
|
36935
|
+
const done = new Promise((res0, rej0) => {
|
|
36936
|
+
const resolve = () => {
|
|
36937
|
+
settled = { ok: true };
|
|
36938
|
+
res0();
|
|
36939
|
+
};
|
|
36940
|
+
const reject = (e) => {
|
|
36941
|
+
settled = { err: e };
|
|
36942
|
+
rej0(e);
|
|
36943
|
+
};
|
|
36944
|
+
timer = setTimeout(() => reject(new Error("expired")), opts.timeoutMs ?? 5 * 6e4);
|
|
36945
|
+
server.on("request", (req, res) => {
|
|
36946
|
+
const html = (status, body) => {
|
|
36947
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Frame-Options": "DENY" });
|
|
36948
|
+
res.end(body);
|
|
36949
|
+
};
|
|
36950
|
+
const u = new URL(req.url ?? "/", origin);
|
|
36951
|
+
if (u.pathname !== route || used) {
|
|
36952
|
+
html(404, PAGE("Esta p\xE1gina ya no est\xE1 disponible.", "Vuelve a Claude y pide guardar el secreto otra vez."));
|
|
36953
|
+
return;
|
|
36954
|
+
}
|
|
36955
|
+
if (req.method === "GET") {
|
|
36956
|
+
html(200, FORM_PAGE(opts.app, opts.name, opts.hint));
|
|
36957
|
+
return;
|
|
36958
|
+
}
|
|
36959
|
+
if (req.method !== "POST") {
|
|
36960
|
+
res.writeHead(405, { Allow: "GET, POST" }).end();
|
|
36961
|
+
return;
|
|
36962
|
+
}
|
|
36963
|
+
const from = req.headers.origin;
|
|
36964
|
+
if (from && from !== origin) {
|
|
36965
|
+
html(403, PAGE("Esa petici\xF3n no viene de esta p\xE1gina.", "Vuelve a Claude y pide guardar el secreto otra vez."));
|
|
36966
|
+
return;
|
|
36967
|
+
}
|
|
36968
|
+
const chunks = [];
|
|
36969
|
+
let size = 0;
|
|
36970
|
+
let tooBig = false;
|
|
36971
|
+
req.on("data", (c) => {
|
|
36972
|
+
size += c.length;
|
|
36973
|
+
if (size > SECRET_MAX_BYTES * 3 + 1024) {
|
|
36974
|
+
tooBig = true;
|
|
36975
|
+
req.destroy();
|
|
36976
|
+
return;
|
|
36977
|
+
}
|
|
36978
|
+
chunks.push(c);
|
|
36979
|
+
});
|
|
36980
|
+
req.on("end", () => {
|
|
36981
|
+
if (tooBig) return;
|
|
36982
|
+
const body = new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
|
|
36983
|
+
let value = body.get("value") ?? "";
|
|
36984
|
+
value = value.replace(/\r?\n$/, "");
|
|
36985
|
+
if (value === "") {
|
|
36986
|
+
html(400, FORM_PAGE(opts.app, opts.name, opts.hint, "Pega el valor antes de guardar."));
|
|
36987
|
+
return;
|
|
36988
|
+
}
|
|
36989
|
+
if (Buffer.byteLength(value, "utf8") > SECRET_MAX_BYTES) {
|
|
36990
|
+
html(400, FORM_PAGE(opts.app, opts.name, opts.hint, "El valor es demasiado largo (m\xE1ximo 8 KB)."));
|
|
36991
|
+
return;
|
|
36992
|
+
}
|
|
36993
|
+
used = true;
|
|
36994
|
+
opts.onValue(value).then(() => {
|
|
36995
|
+
html(200, PAGE("Listo, ya puedes volver a Claude.", `El secreto ${escapeHtml(opts.name)} qued\xF3 guardado para \xAB${escapeHtml(opts.app)}\xBB. Puedes cerrar esta pesta\xF1a.`));
|
|
36996
|
+
clearTimeout(timer);
|
|
36997
|
+
resolve();
|
|
36998
|
+
}).catch((err) => {
|
|
36999
|
+
html(500, PAGE("No se pudo guardar el secreto.", "Vuelve a Claude: ah\xED te digo qu\xE9 pas\xF3 y lo intentamos de nuevo."));
|
|
37000
|
+
clearTimeout(timer);
|
|
37001
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
37002
|
+
});
|
|
37003
|
+
});
|
|
37004
|
+
});
|
|
37005
|
+
});
|
|
37006
|
+
done.catch(() => void 0);
|
|
37007
|
+
try {
|
|
37008
|
+
if (!await opts.openUrl(url2)) {
|
|
37009
|
+
const state0 = settled;
|
|
37010
|
+
if (state0?.err) throw state0.err;
|
|
37011
|
+
if (!state0) throw new SecretPageBrowserUnavailable();
|
|
37012
|
+
}
|
|
37013
|
+
await done;
|
|
37014
|
+
return { url: url2 };
|
|
37015
|
+
} finally {
|
|
37016
|
+
clearTimeout(timer);
|
|
37017
|
+
server.close();
|
|
37018
|
+
server.closeAllConnections?.();
|
|
37019
|
+
}
|
|
37020
|
+
}
|
|
37021
|
+
function escapeHtml(s) {
|
|
37022
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
37023
|
+
}
|
|
37024
|
+
var STYLE = `
|
|
37025
|
+
:root{--primary:#FF6900;--bg:#FEF9F8;--card:#fff;--fg:#0f172a;--muted:#64748b;--border:#e7e0dc;--danger:#b91c1c}
|
|
37026
|
+
*{box-sizing:border-box}
|
|
37027
|
+
body{margin:0;min-height:100vh;display:grid;place-items:center;background:var(--bg);color:var(--fg);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
37028
|
+
main{width:min(30rem,calc(100vw - 2rem));background:var(--card);border:1px solid var(--border);border-radius:20px;padding:2rem}
|
|
37029
|
+
.brand{display:flex;align-items:center;gap:.5rem;color:var(--muted);font-size:12px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;margin-bottom:1.25rem}
|
|
37030
|
+
.brand i{display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--primary)}
|
|
37031
|
+
h1{font-size:20px;margin:0 0 .5rem;font-weight:600}
|
|
37032
|
+
p{margin:0 0 1rem;color:var(--muted)}
|
|
37033
|
+
code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#f5efec;padding:.1rem .4rem;border-radius:6px;color:var(--fg)}
|
|
37034
|
+
label{display:block;font-size:13px;font-weight:600;margin:1rem 0 .35rem}
|
|
37035
|
+
.field{display:flex;gap:.5rem}
|
|
37036
|
+
input{flex:1;min-width:0;height:44px;padding:0 .75rem;font:inherit;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;border:1px solid var(--border);border-radius:12px;background:#fff;color:var(--fg)}
|
|
37037
|
+
input:focus-visible,button:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
|
|
37038
|
+
button{height:44px;padding:0 1rem;font:inherit;font-weight:600;border-radius:12px;border:1px solid var(--border);background:#fff;color:var(--fg);cursor:pointer}
|
|
37039
|
+
button.primary{background:var(--primary);border-color:var(--primary);color:#fff;width:100%;margin-top:1.25rem}
|
|
37040
|
+
.error{color:var(--danger);font-size:13px;margin:.5rem 0 0}
|
|
37041
|
+
.note{font-size:13px;margin-top:1rem}
|
|
37042
|
+
`;
|
|
37043
|
+
function shell(title, body) {
|
|
37044
|
+
return `<!doctype html><html lang="es"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>${escapeHtml(title)}</title><style>${STYLE}</style></head><body><main><div class="brand"><i></i>Kigo Sindri</div>${body}</main></body></html>`;
|
|
37045
|
+
}
|
|
37046
|
+
function FORM_PAGE(app, name, hint, error61) {
|
|
37047
|
+
const title = `Guardar secreto para \xAB${app}\xBB`;
|
|
37048
|
+
return shell(
|
|
37049
|
+
title,
|
|
37050
|
+
`<h1>${escapeHtml(title)}</h1>
|
|
37051
|
+
<p>Clave: <code>${escapeHtml(name)}</code></p>
|
|
37052
|
+
${hint ? `<p>${escapeHtml(hint)}</p>` : ""}
|
|
37053
|
+
<form method="post" autocomplete="off">
|
|
37054
|
+
<label for="value">Valor</label>
|
|
37055
|
+
<div class="field"><input id="value" name="value" type="password" autofocus autocomplete="off" autocapitalize="off" spellcheck="false" required maxlength="8192"><button type="button" id="toggle" aria-pressed="false">mostrar</button></div>
|
|
37056
|
+
${error61 ? `<p class="error" role="alert">${escapeHtml(error61)}</p>` : ""}
|
|
37057
|
+
<button type="submit" class="primary">Guardar</button>
|
|
37058
|
+
</form>
|
|
37059
|
+
<p class="note">Se guarda en tu computadora y en Sindri. Claude no lo ve ni lo repite.</p>
|
|
37060
|
+
<script>(function(){var i=document.getElementById("value"),b=document.getElementById("toggle");b.addEventListener("click",function(){var show=i.type==="password";i.type=show?"text":"password";b.textContent=show?"ocultar":"mostrar";b.setAttribute("aria-pressed",String(show));i.focus();});})();</script>`
|
|
37061
|
+
);
|
|
37062
|
+
}
|
|
37063
|
+
function PAGE(title, text) {
|
|
37064
|
+
return shell(title, `<h1>${escapeHtml(title)}</h1><p>${text}</p>`);
|
|
37065
|
+
}
|
|
37066
|
+
|
|
36733
37067
|
// src/tree.ts
|
|
36734
37068
|
import { createHash as createHash2 } from "node:crypto";
|
|
36735
|
-
import
|
|
37069
|
+
import fs9 from "node:fs/promises";
|
|
36736
37070
|
import os3 from "node:os";
|
|
36737
|
-
import
|
|
37071
|
+
import path9 from "node:path";
|
|
36738
37072
|
var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".sindri", "dist", ".turbo", ".git"]);
|
|
36739
37073
|
function excluded(name) {
|
|
36740
37074
|
if (EXCLUDED_DIRS.has(name)) return true;
|
|
@@ -36745,11 +37079,11 @@ function excluded(name) {
|
|
|
36745
37079
|
async function listFiles(root) {
|
|
36746
37080
|
const out = [];
|
|
36747
37081
|
async function walk(dir, rel) {
|
|
36748
|
-
const entries = await
|
|
37082
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
36749
37083
|
for (const e of entries) {
|
|
36750
37084
|
if (excluded(e.name)) continue;
|
|
36751
37085
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
36752
|
-
if (e.isDirectory()) await walk(
|
|
37086
|
+
if (e.isDirectory()) await walk(path9.join(dir, e.name), r);
|
|
36753
37087
|
else if (e.isFile()) out.push(r);
|
|
36754
37088
|
}
|
|
36755
37089
|
}
|
|
@@ -36760,7 +37094,7 @@ async function treeHash(root) {
|
|
|
36760
37094
|
const files = await listFiles(root);
|
|
36761
37095
|
const h = createHash2("sha256");
|
|
36762
37096
|
for (const rel of files) {
|
|
36763
|
-
const fh = createHash2("sha256").update(await
|
|
37097
|
+
const fh = createHash2("sha256").update(await fs9.readFile(path9.join(root, rel))).digest("hex");
|
|
36764
37098
|
h.update(`${rel}\0${fh}
|
|
36765
37099
|
`);
|
|
36766
37100
|
}
|
|
@@ -36769,49 +37103,49 @@ async function treeHash(root) {
|
|
|
36769
37103
|
async function packTree(root) {
|
|
36770
37104
|
const files = await listFiles(root);
|
|
36771
37105
|
if (files.length === 0) throw new Error("empty tree");
|
|
36772
|
-
const listFile =
|
|
36773
|
-
await
|
|
36774
|
-
const out =
|
|
37106
|
+
const listFile = path9.join(await fs9.mkdtemp(path9.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
|
|
37107
|
+
await fs9.writeFile(listFile, files.join("\n") + "\n");
|
|
37108
|
+
const out = path9.join(path9.dirname(listFile), "app.tar.gz");
|
|
36775
37109
|
const r = await run("tar", ["-czf", out, "-C", root, "-T", listFile], { timeoutMs: 12e4 });
|
|
36776
37110
|
if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
|
|
36777
|
-
const st = await
|
|
37111
|
+
const st = await fs9.stat(out);
|
|
36778
37112
|
return { file: out, files: files.length, bytes: st.size };
|
|
36779
37113
|
}
|
|
36780
37114
|
async function replaceTree(root, archive) {
|
|
36781
|
-
const tmp = await
|
|
37115
|
+
const tmp = await fs9.mkdtemp(path9.join(os3.tmpdir(), "sindri-unpack-"));
|
|
36782
37116
|
const r = await run("tar", ["-xzf", archive, "-C", tmp], { timeoutMs: 12e4 });
|
|
36783
37117
|
if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
|
|
36784
37118
|
const src = await singleRoot(tmp);
|
|
36785
|
-
await
|
|
37119
|
+
await fs9.mkdir(root, { recursive: true });
|
|
36786
37120
|
await prune(root, 0);
|
|
36787
|
-
await
|
|
36788
|
-
await
|
|
37121
|
+
await fs9.cp(src, root, { recursive: true, force: true, filter: (s) => path9.basename(s) !== "node_modules" && path9.basename(s) !== ".DS_Store" });
|
|
37122
|
+
await fs9.rm(tmp, { recursive: true, force: true });
|
|
36789
37123
|
}
|
|
36790
37124
|
async function prune(dir, depth) {
|
|
36791
|
-
const entries = await
|
|
37125
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
36792
37126
|
for (const e of entries) {
|
|
36793
|
-
const p =
|
|
37127
|
+
const p = path9.join(dir, e.name);
|
|
36794
37128
|
if (e.name === "node_modules") continue;
|
|
36795
37129
|
if (depth === 3 && (e.name === ".sindri" || /^\.env(\..*)?$/.test(e.name))) continue;
|
|
36796
37130
|
if (e.isDirectory()) {
|
|
36797
37131
|
await prune(p, depth + 1);
|
|
36798
|
-
const left = await
|
|
36799
|
-
if (left.length === 0) await
|
|
37132
|
+
const left = await fs9.readdir(p);
|
|
37133
|
+
if (left.length === 0) await fs9.rmdir(p);
|
|
36800
37134
|
} else {
|
|
36801
|
-
await
|
|
37135
|
+
await fs9.rm(p, { force: true });
|
|
36802
37136
|
}
|
|
36803
37137
|
}
|
|
36804
37138
|
}
|
|
36805
37139
|
async function singleRoot(dir) {
|
|
36806
|
-
const entries = (await
|
|
36807
|
-
if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(
|
|
36808
|
-
return
|
|
37140
|
+
const entries = (await fs9.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
|
|
37141
|
+
if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path9.join(dir, "package.json"))) {
|
|
37142
|
+
return path9.join(dir, entries[0].name);
|
|
36809
37143
|
}
|
|
36810
37144
|
return dir;
|
|
36811
37145
|
}
|
|
36812
37146
|
async function exists2(p) {
|
|
36813
37147
|
try {
|
|
36814
|
-
await
|
|
37148
|
+
await fs9.access(p);
|
|
36815
37149
|
return true;
|
|
36816
37150
|
} catch {
|
|
36817
37151
|
return false;
|
|
@@ -36819,14 +37153,14 @@ async function exists2(p) {
|
|
|
36819
37153
|
}
|
|
36820
37154
|
async function readPublished(root) {
|
|
36821
37155
|
try {
|
|
36822
|
-
return JSON.parse(await
|
|
37156
|
+
return JSON.parse(await fs9.readFile(path9.join(root, ".sindri", "last-published.json"), "utf8"));
|
|
36823
37157
|
} catch {
|
|
36824
37158
|
return null;
|
|
36825
37159
|
}
|
|
36826
37160
|
}
|
|
36827
37161
|
async function writePublished(root, p) {
|
|
36828
|
-
await
|
|
36829
|
-
await
|
|
37162
|
+
await fs9.mkdir(path9.join(root, ".sindri"), { recursive: true });
|
|
37163
|
+
await fs9.writeFile(path9.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
|
|
36830
37164
|
}
|
|
36831
37165
|
|
|
36832
37166
|
// src/tools.ts
|
|
@@ -36859,7 +37193,7 @@ var SCHEMAS = {
|
|
|
36859
37193
|
preview: { app: appName },
|
|
36860
37194
|
publish: {
|
|
36861
37195
|
app: appName,
|
|
36862
|
-
message: external_exports.string().min(3).max(MESSAGE_MAX).describe("What changed, in Spanish, as the
|
|
37196
|
+
message: external_exports.string().min(3).max(MESSAGE_MAX).describe("What changed, in Spanish, as the builder would say it (becomes the version label).")
|
|
36863
37197
|
},
|
|
36864
37198
|
status: { app: appName },
|
|
36865
37199
|
list_apps: {},
|
|
@@ -36869,27 +37203,42 @@ var SCHEMAS = {
|
|
|
36869
37203
|
rename_app: {
|
|
36870
37204
|
app: appName,
|
|
36871
37205
|
new_name: appName.optional().describe("New address: lowercase letters, digits and hyphens (2 to 31 chars). Becomes https://<new_name>.sindri.kigo.dev; the old address stops working."),
|
|
36872
|
-
display_name: external_exports.string().min(2).max(60).optional().describe("New visible name (the title people see), in Spanish, as the
|
|
37206
|
+
display_name: external_exports.string().min(2).max(60).optional().describe("New visible name (the title people see), in Spanish, as the builder wrote it. The address does not change.")
|
|
36873
37207
|
},
|
|
36874
37208
|
delete_app: {
|
|
36875
37209
|
app: appName,
|
|
36876
|
-
confirm: external_exports.string().describe("The app name exactly as the
|
|
37210
|
+
confirm: external_exports.string().describe("The app name exactly as the builder typed it to confirm. Must equal `app`; never fill it in for them.")
|
|
36877
37211
|
},
|
|
36878
37212
|
share: { app: appName },
|
|
36879
37213
|
help: {},
|
|
36880
37214
|
design_guide: {
|
|
36881
37215
|
app: appName,
|
|
36882
37216
|
topic: external_exports.string().min(1).max(60).optional().describe("A reference topic from `topics` (admin, landing, mini-app, mobile, tokens, writing, checklist). Omit for the overview (SKILL.md) and the list of topics.")
|
|
37217
|
+
},
|
|
37218
|
+
set_secret: {
|
|
37219
|
+
app: appName,
|
|
37220
|
+
name: external_exports.string().min(2).max(64).describe("Environment variable name the code reads as process.env.NAME: uppercase letters, digits and underscores (e.g. LINEAR_API_KEY). Platform keys are refused."),
|
|
37221
|
+
hint: external_exports.string().min(3).max(200).optional().describe("One line in Spanish shown on the page telling the builder what to paste (e.g. \xABla clave de API de Linear, solo lectura\xBB).")
|
|
37222
|
+
},
|
|
37223
|
+
list_secrets: { app: appName },
|
|
37224
|
+
delete_secret: {
|
|
37225
|
+
app: appName,
|
|
37226
|
+
name: external_exports.string().min(2).max(64).describe("The secret's name, as listed by list_secrets.")
|
|
37227
|
+
},
|
|
37228
|
+
run_task: {
|
|
37229
|
+
app: appName,
|
|
37230
|
+
task: external_exports.string().min(2).max(32).describe("Task name as declared in the `schedules` line of kigo-app.yaml."),
|
|
37231
|
+
where: external_exports.enum(["local", "sindri"]).optional().describe("Where to run it: `local` calls the route of the running preview on this computer; `sindri` runs it in the published app. Default: local when a preview is running, else sindri.")
|
|
36883
37232
|
}
|
|
36884
37233
|
};
|
|
36885
37234
|
var DESCRIPTIONS = {
|
|
36886
37235
|
setup: {
|
|
36887
37236
|
title: "Set up this computer",
|
|
36888
|
-
description: "Installs what Sindri needs on the
|
|
37237
|
+
description: "Installs what Sindri needs on the builder's computer (Node.js and pnpm, in the user's home, no admin rights) and creates the Sindri folder. Run it once per machine, before the first create_app, or whenever another tool answers needs_setup. Safe to repeat."
|
|
36889
37238
|
},
|
|
36890
37239
|
login: {
|
|
36891
37240
|
title: "Sign in to Kigo",
|
|
36892
|
-
description: "Signs the
|
|
37241
|
+
description: "Signs the builder in with their Kigo account (opens the browser; they enter with their @cargomovil.com Google account). Run it when another tool answers needs_login. Credentials are stored on this computer."
|
|
36893
37242
|
},
|
|
36894
37243
|
create_app: {
|
|
36895
37244
|
title: "Create an app",
|
|
@@ -36897,7 +37246,7 @@ var DESCRIPTIONS = {
|
|
|
36897
37246
|
},
|
|
36898
37247
|
preview: {
|
|
36899
37248
|
title: "Preview locally",
|
|
36900
|
-
description: "Runs the app on this computer with test data and opens it in the browser (http://127.0.0.1:<port>). Reuses a running preview. Use it to show the
|
|
37249
|
+
description: "Runs the app on this computer with test data and opens it in the browser (http://127.0.0.1:<port>). Reuses a running preview. Use it to show the builder changes before publishing; edits reload automatically."
|
|
36901
37250
|
},
|
|
36902
37251
|
publish: {
|
|
36903
37252
|
title: "Publish to Sindri",
|
|
@@ -36907,7 +37256,7 @@ var DESCRIPTIONS = {
|
|
|
36907
37256
|
title: "App status",
|
|
36908
37257
|
description: "Address, last published version, whether the local folder exists and whether it has unpublished changes."
|
|
36909
37258
|
},
|
|
36910
|
-
list_apps: { title: "List apps", description: "The
|
|
37259
|
+
list_apps: { title: "List apps", description: "The builder's apps in Sindri, with address, last version and whether each has a local folder." },
|
|
36911
37260
|
versions: { title: "Versions", description: "Published versions of an app, most recent first (id, date, label)." },
|
|
36912
37261
|
rollback: {
|
|
36913
37262
|
title: "Go back to a version",
|
|
@@ -36915,24 +37264,40 @@ var DESCRIPTIONS = {
|
|
|
36915
37264
|
},
|
|
36916
37265
|
update_template: {
|
|
36917
37266
|
title: "Update the template",
|
|
36918
|
-
description: "Brings the app up to the current Kigo template: files the app never customised are replaced, files you changed are left alone and listed. Publishes a version \xABPlantilla actualizada\xBB and brings the local folder to it. Use it when the
|
|
37267
|
+
description: "Brings the app up to the current Kigo template: files the app never customised are replaced, files you changed are left alone and listed. Publishes a version \xABPlantilla actualizada\xBB and brings the local folder to it. Use it when the builder asks to update the template or Sindri announces a template fix. Refuses while there are unpublished local edits: publish (or discard) them first."
|
|
36919
37268
|
},
|
|
36920
37269
|
rename_app: {
|
|
36921
37270
|
title: "Rename an app",
|
|
36922
|
-
description: 'Two different things, pass at least one. `new_name` changes the address (https://<new_name>.sindri.kigo.dev): Sindri renames it, the local folder is moved to match and the old address stops working; refuses while the folder has unpublished edits (publish first). `display_name` changes only the visible name: it is written to kigo-app.yaml and published as \xABAhora se llama \xAB<display_name>\xBB\xBB. When the
|
|
37271
|
+
description: 'Two different things, pass at least one. `new_name` changes the address (https://<new_name>.sindri.kigo.dev): Sindri renames it, the local folder is moved to match and the old address stops working; refuses while the folder has unpublished edits (publish first). `display_name` changes only the visible name: it is written to kigo-app.yaml and published as \xABAhora se llama \xAB<display_name>\xBB\xBB. When the builder says "c\xE1mbiale el nombre" and it is unclear which one they mean, ask: \xBFla direcci\xF3n o el nombre que se ve? Both together are allowed (visible name first, then the address).'
|
|
36923
37272
|
},
|
|
36924
37273
|
delete_app: {
|
|
36925
37274
|
title: "Delete an app",
|
|
36926
|
-
description: "Removes the app from Sindri (address, published versions and its database) after the
|
|
37275
|
+
description: "Removes the app from Sindri (address, published versions and its database) after the builder typed its name: `confirm` must equal `app`, ask them to write it, never guess it. Stops the preview and moves the local folder to ~/Sindri/.papelera/<app>-<date> instead of deleting it, so the builder can still recover the files by hand."
|
|
36927
37276
|
},
|
|
36928
37277
|
share: { title: "Share", description: "The line to paste in Slack: address, test-data notice and how the team enters." },
|
|
36929
37278
|
help: {
|
|
36930
37279
|
title: "What Sindri is",
|
|
36931
|
-
description: "No network. The Spanish introduction to give the
|
|
37280
|
+
description: "No network. The Spanish introduction to give the builder the first time they speak in a conversation or whenever they ask what Sindri can do: what it is, what they can ask for, the limits and the link to Mi espacio. Also says whether they are already signed in on this computer (`logged_in`) and where their apps live (`apps_dir`)."
|
|
36932
37281
|
},
|
|
36933
37282
|
design_guide: {
|
|
36934
37283
|
title: "Design guide",
|
|
36935
|
-
description: "No network. Kigo's design guide for Sindri apps, read from the app folder (the kigo-sindri-design skill the template ships). Call it before creating or changing any page, component, layout, style or copy: first without `topic` (the overview plus the list of `topics`), then with the topic that matches what you are building (admin, landing, mini-app, mobile). `content` is markdown for you to follow, not to paste to the
|
|
37284
|
+
description: "No network. Kigo's design guide for Sindri apps, read from the app folder (the kigo-sindri-design skill the template ships). Call it before creating or changing any page, component, layout, style or copy: first without `topic` (the overview plus the list of `topics`), then with the topic that matches what you are building (admin, landing, mini-app, mobile). `content` is markdown for you to follow, not to paste to the builder. If it answers `no_design_guide`, the app predates the guide: run update_template once and retry."
|
|
37285
|
+
},
|
|
37286
|
+
set_secret: {
|
|
37287
|
+
title: "Save a secret",
|
|
37288
|
+
description: "Asks the builder for a credential (API key, token, password) without it ever passing through the conversation: opens a page in their browser where they paste the value, stores it in the app's .env.local on this computer and in Sindri, and restarts the running preview so the code sees it. Call it whenever the app needs a credential; never ask the builder to paste it in chat and never write it in code. Refer to it in code as process.env.NAME. Waits up to five minutes. The result never contains the value."
|
|
37289
|
+
},
|
|
37290
|
+
list_secrets: {
|
|
37291
|
+
title: "List secrets",
|
|
37292
|
+
description: "Names (never values) of the app's secrets in Sindri (`names`) and in the .env.local on this computer (`local_names`), so you can tell when one side is missing. Platform keys are not listed."
|
|
37293
|
+
},
|
|
37294
|
+
delete_secret: {
|
|
37295
|
+
title: "Delete a secret",
|
|
37296
|
+
description: "Removes a secret from the app's .env.local on this computer and from Sindri, and restarts the running preview. Ask the builder before calling it."
|
|
37297
|
+
},
|
|
37298
|
+
run_task: {
|
|
37299
|
+
title: "Run a scheduled task now",
|
|
37300
|
+
description: 'Runs one of the app\'s scheduled tasks (declared in the `schedules` line of kigo-app.yaml) and reports status, HTTP status, duration and the tail of its output. `where: "local"` calls the route of the preview running on this computer with the local task token; `where: "sindri"` runs it in the published app. Default: local when a preview is running, else sindri. Test a task locally before publishing it.'
|
|
36936
37301
|
}
|
|
36937
37302
|
};
|
|
36938
37303
|
function createTools(deps) {
|
|
@@ -36958,7 +37323,15 @@ function createTools(deps) {
|
|
|
36958
37323
|
};
|
|
36959
37324
|
}
|
|
36960
37325
|
const setup = guard(async () => {
|
|
36961
|
-
|
|
37326
|
+
let report;
|
|
37327
|
+
try {
|
|
37328
|
+
report = await setupToolchain(cfg, { env, ...deps.installer ? { installer: deps.installer } : {} });
|
|
37329
|
+
} catch (err) {
|
|
37330
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37331
|
+
process.stderr.write(`sindri: ${msg}
|
|
37332
|
+
`);
|
|
37333
|
+
return { ok: false, summary: "No pude instalar Node y pnpm en tu computadora. Escribe en Slack, canal #sindri, y pega el detalle.", detail: msg.slice(0, 400) };
|
|
37334
|
+
}
|
|
36962
37335
|
const what = report.installed.length ? `Instal\xE9 ${report.installed.join(" y ")}.` : "Tu computadora ya ten\xEDa lo necesario.";
|
|
36963
37336
|
return {
|
|
36964
37337
|
ok: true,
|
|
@@ -37027,14 +37400,14 @@ function createTools(deps) {
|
|
|
37027
37400
|
}
|
|
37028
37401
|
const created = await api.createApp(a.name, a.description.trim());
|
|
37029
37402
|
const archive = await api.download(created.archive_url);
|
|
37030
|
-
await
|
|
37403
|
+
await fs10.mkdir(p.root, { recursive: true });
|
|
37031
37404
|
await replaceTree(p.root, archive);
|
|
37032
|
-
await
|
|
37405
|
+
await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
|
|
37033
37406
|
p = await appPaths(cfg, a.name, me.owner);
|
|
37034
37407
|
if (!await appPresent(p)) {
|
|
37035
37408
|
return { ok: false, summary: `Cre\xE9 "${a.name}" en Sindri pero lo que baj\xF3 no trae la app donde la espero (apps/${p.owner || me.owner}/${a.name}). Escribe en Slack, canal #sindri.`, path: p.root, url: created.url };
|
|
37036
37409
|
}
|
|
37037
|
-
await
|
|
37410
|
+
await ensureLocalEnv(p.appDir, a.name);
|
|
37038
37411
|
const install = await run(tc.pnpm, [...PNPM_ARGS, "install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
|
|
37039
37412
|
if (install.code !== 0) {
|
|
37040
37413
|
const why = lastLine(install.stderr || install.stdout);
|
|
@@ -37064,7 +37437,7 @@ function createTools(deps) {
|
|
|
37064
37437
|
}
|
|
37065
37438
|
const tc = await toolchain();
|
|
37066
37439
|
if (!tc) return NEEDS_SETUP;
|
|
37067
|
-
if (!await exists2(
|
|
37440
|
+
if (!await exists2(path10.join(p.root, "node_modules"))) {
|
|
37068
37441
|
const install = await run(tc.pnpm, [...PNPM_ARGS, "install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
|
|
37069
37442
|
if (install.code !== 0) return { ok: false, summary: `No pude preparar la app para correrla en tu computadora (${lastLine(install.stderr || install.stdout)}). Vuelve a intentarlo.`, detail: lastLine(install.stderr || install.stdout) };
|
|
37070
37443
|
}
|
|
@@ -37105,14 +37478,14 @@ function createTools(deps) {
|
|
|
37105
37478
|
}
|
|
37106
37479
|
const packed = await packTree(dir);
|
|
37107
37480
|
if (packed.bytes > 20 * 1024 * 1024) {
|
|
37108
|
-
await
|
|
37481
|
+
await fs10.rm(path10.dirname(packed.file), { recursive: true, force: true });
|
|
37109
37482
|
return { ok: false, summary: "La app pesa m\xE1s de 20 MB sin contar sus piezas instaladas. Quita archivos grandes (im\xE1genes, videos) antes de publicar.", bytes: packed.bytes };
|
|
37110
37483
|
}
|
|
37111
37484
|
let result;
|
|
37112
37485
|
try {
|
|
37113
37486
|
result = await api.publish(a.app, packed.file, message);
|
|
37114
37487
|
} finally {
|
|
37115
|
-
await
|
|
37488
|
+
await fs10.rm(path10.dirname(packed.file), { recursive: true, force: true });
|
|
37116
37489
|
}
|
|
37117
37490
|
await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
|
|
37118
37491
|
return {
|
|
@@ -37131,6 +37504,8 @@ function createTools(deps) {
|
|
|
37131
37504
|
if (!local.exists) parts.push("En esta computadora no est\xE1 su carpeta.");
|
|
37132
37505
|
else if (local.unpublished) parts.push("Tienes cambios en tu computadora que a\xFAn no publicas.");
|
|
37133
37506
|
else parts.push("Tu carpeta est\xE1 al d\xEDa con lo publicado.");
|
|
37507
|
+
const tasks = describeTasks(info);
|
|
37508
|
+
if (tasks) parts.push(tasks);
|
|
37134
37509
|
return { ok: true, summary: parts.join(" "), app: info, local };
|
|
37135
37510
|
});
|
|
37136
37511
|
const list_apps = guard(async () => {
|
|
@@ -37154,16 +37529,16 @@ function createTools(deps) {
|
|
|
37154
37529
|
let p = await appPaths(cfg, a.app);
|
|
37155
37530
|
await stopPreview(cfg, a.app);
|
|
37156
37531
|
const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version.id);
|
|
37157
|
-
await
|
|
37532
|
+
await fs10.mkdir(p.root, { recursive: true });
|
|
37158
37533
|
await replaceTree(p.root, archive);
|
|
37159
|
-
await
|
|
37534
|
+
await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
|
|
37160
37535
|
p = await appPaths(cfg, a.app, p.owner || void 0);
|
|
37161
37536
|
const dir = p.appDir;
|
|
37162
37537
|
if (!await appPresent(p)) return { ok: false, summary: `Sindri volvi\xF3 a la versi\xF3n pero lo que baj\xF3 no trae la app donde la espero. Escribe en Slack, canal #sindri.`, version: result.version, url: result.url };
|
|
37163
|
-
|
|
37538
|
+
await ensureLocalEnv(dir, a.app);
|
|
37164
37539
|
await linkDesignGuide(p);
|
|
37165
37540
|
const tc = await toolchain();
|
|
37166
|
-
if (tc && await exists2(
|
|
37541
|
+
if (tc && await exists2(path10.join(p.root, "node_modules"))) {
|
|
37167
37542
|
const db = await prepareLocalDb(tc, dir);
|
|
37168
37543
|
if (!db.ok) process.stderr.write(`sindri: local db after rollback: ${db.detail}
|
|
37169
37544
|
`);
|
|
@@ -37193,14 +37568,14 @@ function createTools(deps) {
|
|
|
37193
37568
|
await stopPreview(cfg, a.app);
|
|
37194
37569
|
const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version?.id);
|
|
37195
37570
|
await replaceTree(p.root, archive);
|
|
37196
|
-
await
|
|
37571
|
+
await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
|
|
37197
37572
|
p = await appPaths(cfg, a.app, p.owner || void 0);
|
|
37198
37573
|
const dir = p.appDir;
|
|
37199
37574
|
if (!await appPresent(p)) return { ok: false, summary: `Sindri actualiz\xF3 la plantilla pero lo que baj\xF3 no trae la app donde la espero. Escribe en Slack, canal #sindri.`, ...data };
|
|
37200
|
-
|
|
37575
|
+
await ensureLocalEnv(dir, a.app);
|
|
37201
37576
|
await linkDesignGuide(p);
|
|
37202
37577
|
const tc = await toolchain();
|
|
37203
|
-
if (tc && await exists2(
|
|
37578
|
+
if (tc && await exists2(path10.join(p.root, "node_modules"))) {
|
|
37204
37579
|
const db = await prepareLocalDb(tc, dir);
|
|
37205
37580
|
if (!db.ok) process.stderr.write(`sindri: local db after update_template: ${db.detail}
|
|
37206
37581
|
`);
|
|
@@ -37252,9 +37627,10 @@ function createTools(deps) {
|
|
|
37252
37627
|
await stopPreview(cfg, a.app);
|
|
37253
37628
|
const newRoot = appRoot(cfg, newName);
|
|
37254
37629
|
await moveDir(p.root, newRoot);
|
|
37255
|
-
const newDir =
|
|
37256
|
-
await moveDir(
|
|
37630
|
+
const newDir = path10.join(newRoot, "apps", p.owner, newName);
|
|
37631
|
+
await moveDir(path10.join(newRoot, "apps", p.owner, a.app), newDir);
|
|
37257
37632
|
await writeManifestValue(newDir, "name", newName);
|
|
37633
|
+
await writeEnvFile(newDir, upsertEnv(await readEnvFile(newDir), "KIGO_APP_NAME", newName));
|
|
37258
37634
|
await writePublished(newDir, { version: result.version ?? before.published_version, tree_hash: await treeHash(newDir), url: result.url });
|
|
37259
37635
|
await linkDesignGuide({ root: newRoot, appDir: newDir, owner: p.owner });
|
|
37260
37636
|
data.path = newDir;
|
|
@@ -37281,7 +37657,7 @@ function createTools(deps) {
|
|
|
37281
37657
|
}
|
|
37282
37658
|
let trash = null;
|
|
37283
37659
|
if (present) {
|
|
37284
|
-
trash =
|
|
37660
|
+
trash = path10.join(cfg.home, ".papelera", `${a.app}-${stamp()}`);
|
|
37285
37661
|
await moveDir(root, trash);
|
|
37286
37662
|
}
|
|
37287
37663
|
const kept = "Una copia de sus archivos queda en tu computadora, en la papelera de Sindri; puedes borrarla a mano cuando quieras.";
|
|
@@ -37324,21 +37700,147 @@ function createTools(deps) {
|
|
|
37324
37700
|
content: guide.content
|
|
37325
37701
|
};
|
|
37326
37702
|
});
|
|
37703
|
+
async function restartPreview(app, dir) {
|
|
37704
|
+
if (!await runningPreview(cfg, app, fetchImpl)) return false;
|
|
37705
|
+
const tc = await toolchain();
|
|
37706
|
+
if (!tc) return false;
|
|
37707
|
+
await stopPreview(cfg, app);
|
|
37708
|
+
const r = await startPreview(cfg, tc, app, dir, { fetchImpl, ...deps.sleepImpl ? { sleepImpl: deps.sleepImpl } : {} });
|
|
37709
|
+
return r.log === void 0;
|
|
37710
|
+
}
|
|
37711
|
+
function checkSecretName(name) {
|
|
37712
|
+
if (!validSecretName(name)) {
|
|
37713
|
+
return { ok: false, summary: `"${name}" no sirve como nombre de secreto: va en MAY\xDASCULAS, con n\xFAmeros o guiones bajos, de 2 a 64 letras (por ejemplo LINEAR_API_KEY).`, code: "bad_name" };
|
|
37714
|
+
}
|
|
37715
|
+
if (reservedKey(name)) return { ok: false, summary: `${name} es una clave de la plataforma; no se puede guardar como secreto. Elige otro nombre.`, code: "reserved_key" };
|
|
37716
|
+
return null;
|
|
37717
|
+
}
|
|
37718
|
+
const set_secret = guard(async (a) => {
|
|
37719
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37720
|
+
const name = a.name.trim();
|
|
37721
|
+
const bad = checkSecretName(name);
|
|
37722
|
+
if (bad) return bad;
|
|
37723
|
+
await api.app(a.app);
|
|
37724
|
+
const p = await appPaths(cfg, a.app);
|
|
37725
|
+
const local = await appPresent(p);
|
|
37726
|
+
const openUrl = deps.openUrl ?? defaultOpenUrl(cfg);
|
|
37727
|
+
let restarted = false;
|
|
37728
|
+
try {
|
|
37729
|
+
await collectSecret({
|
|
37730
|
+
app: a.app,
|
|
37731
|
+
name,
|
|
37732
|
+
...a.hint ? { hint: a.hint.trim() } : {},
|
|
37733
|
+
openUrl,
|
|
37734
|
+
...deps.secretTimeoutMs ? { timeoutMs: deps.secretTimeoutMs } : {},
|
|
37735
|
+
onValue: async (value) => {
|
|
37736
|
+
if (local) await writeEnvFile(p.appDir, upsertEnv(await readEnvFile(p.appDir), name, value));
|
|
37737
|
+
restarted = (await api.putSecret(a.app, name, value)).restarted;
|
|
37738
|
+
}
|
|
37739
|
+
});
|
|
37740
|
+
} catch (err) {
|
|
37741
|
+
if (err instanceof SecretPageBrowserUnavailable) return { ok: false, summary: "No pude abrir tu navegador para pedirte el secreto. \xC1brelo t\xFA y vuelve a intentarlo.", code: "no_browser" };
|
|
37742
|
+
if (err instanceof Error && err.message === "expired") {
|
|
37743
|
+
return { ok: false, summary: `Pasaron cinco minutos sin que se guardara ${name}. Cuando lo tengas a mano, p\xEDdeme guardarlo de nuevo.`, code: "expired", name };
|
|
37744
|
+
}
|
|
37745
|
+
throw err;
|
|
37746
|
+
}
|
|
37747
|
+
const previewRestarted = local ? await restartPreview(a.app, p.appDir) : false;
|
|
37748
|
+
const where = local ? "en tu computadora y en Sindri" : "en Sindri (en esta computadora no est\xE1 la carpeta de la app)";
|
|
37749
|
+
const applied = previewRestarted ? " Reinici\xE9 la vista previa para que lo tome." : "";
|
|
37750
|
+
return { ok: true, summary: `Listo, ${name} qued\xF3 guardado ${where}.${applied}`, name, restarted, preview_restarted: previewRestarted, local };
|
|
37751
|
+
});
|
|
37752
|
+
const list_secrets = guard(async (a) => {
|
|
37753
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37754
|
+
const names = (await api.listSecrets(a.app)).sort();
|
|
37755
|
+
const p = await appPaths(cfg, a.app);
|
|
37756
|
+
const localNames = await appPresent(p) ? secretKeys(await readEnvFile(p.appDir)).sort() : [];
|
|
37757
|
+
const missingLocal = names.filter((n) => !localNames.includes(n));
|
|
37758
|
+
const missingInSindri = localNames.filter((n) => !names.includes(n));
|
|
37759
|
+
const parts = [names.length === 0 ? `"${a.app}" no tiene secretos en Sindri.` : `"${a.app}" tiene ${names.length === 1 ? "un secreto" : `${names.length} secretos`} en Sindri: ${names.join(", ")}.`];
|
|
37760
|
+
if (missingLocal.length) parts.push(`En tu computadora ${missingLocal.length === 1 ? "falta" : "faltan"} ${missingLocal.join(", ")}.`);
|
|
37761
|
+
if (missingInSindri.length) parts.push(`En Sindri ${missingInSindri.length === 1 ? "falta" : "faltan"} ${missingInSindri.join(", ")} (solo est\xE1 en tu computadora).`);
|
|
37762
|
+
return { ok: true, summary: parts.join(" "), names, local_names: localNames, missing_local: missingLocal, missing_in_sindri: missingInSindri };
|
|
37763
|
+
});
|
|
37764
|
+
const delete_secret = guard(async (a) => {
|
|
37765
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37766
|
+
const name = a.name.trim();
|
|
37767
|
+
const bad = checkSecretName(name);
|
|
37768
|
+
if (bad) return bad;
|
|
37769
|
+
let inSindri = true;
|
|
37770
|
+
try {
|
|
37771
|
+
await api.deleteSecret(a.app, name);
|
|
37772
|
+
} catch (err) {
|
|
37773
|
+
if (err instanceof ApiError && err.status === 404) inSindri = false;
|
|
37774
|
+
else throw err;
|
|
37775
|
+
}
|
|
37776
|
+
const p = await appPaths(cfg, a.app);
|
|
37777
|
+
let hadLocal = false;
|
|
37778
|
+
if (await appPresent(p)) {
|
|
37779
|
+
const text = await readEnvFile(p.appDir);
|
|
37780
|
+
hadLocal = envValue(text, name) !== void 0;
|
|
37781
|
+
if (hadLocal) await writeEnvFile(p.appDir, removeEnv(text, name));
|
|
37782
|
+
}
|
|
37783
|
+
const previewRestarted = hadLocal ? await restartPreview(a.app, p.appDir) : false;
|
|
37784
|
+
const where = inSindri && hadLocal ? "de Sindri y de tu computadora" : inSindri ? "de Sindri" : hadLocal ? "de tu computadora (en Sindri no estaba)" : "";
|
|
37785
|
+
const summary = where ? `Borr\xE9 ${name} ${where}.${previewRestarted ? " Reinici\xE9 la vista previa." : ""}` : `${name} no estaba guardado ni en Sindri ni en tu computadora.`;
|
|
37786
|
+
return { ok: true, summary, name, removed_from_sindri: inSindri, removed_locally: hadLocal, preview_restarted: previewRestarted };
|
|
37787
|
+
});
|
|
37788
|
+
const run_task = guard(async (a) => {
|
|
37789
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37790
|
+
const task = a.task.trim();
|
|
37791
|
+
const p = await appPaths(cfg, a.app);
|
|
37792
|
+
const running = await runningPreview(cfg, a.app, fetchImpl);
|
|
37793
|
+
const where = a.where ?? (running ? "local" : "sindri");
|
|
37794
|
+
if (where === "sindri") {
|
|
37795
|
+
const r2 = await api.runTask(a.app, task);
|
|
37796
|
+
return { ok: r2.ok, summary: describeRun(task, "en Sindri", r2), where, status: r2.status, http_status: r2.http_status, duration_ms: r2.duration_ms, output_tail: r2.output_tail };
|
|
37797
|
+
}
|
|
37798
|
+
if (!await appPresent(p)) return { ok: false, summary: `No encuentro la carpeta de "${a.app}" en tu computadora; corre la tarea en Sindri (where: "sindri").`, code: "no_app" };
|
|
37799
|
+
const schedules = readSchedules(await readManifest(p.appDir));
|
|
37800
|
+
if (!schedules.ok) return { ok: false, summary: schedules.error, code: "bad_schedules" };
|
|
37801
|
+
const declared = schedules.tasks.find((t) => t.name === task);
|
|
37802
|
+
if (!declared) {
|
|
37803
|
+
const names = schedules.tasks.map((t) => t.name);
|
|
37804
|
+
return { ok: false, summary: names.length ? `La tarea \xAB${task}\xBB no est\xE1 declarada en kigo-app.yaml de "${a.app}". Las declaradas son: ${names.join(", ")}.` : `"${a.app}" no declara tareas en kigo-app.yaml todav\xEDa; a\xF1ade la l\xEDnea schedules y vuelve a intentarlo.`, code: "task_not_declared", tasks: names };
|
|
37805
|
+
}
|
|
37806
|
+
const ensured = await ensureLocalEnv(p.appDir, a.app);
|
|
37807
|
+
if (!running) return { ok: false, summary: `La vista previa de "${a.app}" no est\xE1 corriendo. Arr\xE1ncala con preview y vuelve a correr la tarea, o c\xF3rrela en Sindri (where: "sindri").`, code: "no_preview" };
|
|
37808
|
+
if (ensured.created_token) await restartPreview(a.app, p.appDir);
|
|
37809
|
+
const token = envValue(await readEnvFile(p.appDir), "SINDRI_TASK_TOKEN") ?? "";
|
|
37810
|
+
const port = (await runningPreview(cfg, a.app, fetchImpl))?.port ?? running.port;
|
|
37811
|
+
const started = Date.now();
|
|
37812
|
+
let res = null;
|
|
37813
|
+
let timedOut = false;
|
|
37814
|
+
try {
|
|
37815
|
+
res = await fetchImpl(`http://127.0.0.1:${port}${declared.path}`, {
|
|
37816
|
+
method: "POST",
|
|
37817
|
+
headers: { "X-Sindri-Task": task, Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
37818
|
+
signal: AbortSignal.timeout(LOCAL_TASK_TIMEOUT_MS)
|
|
37819
|
+
});
|
|
37820
|
+
} catch (err) {
|
|
37821
|
+
timedOut = err instanceof Error && err.name === "TimeoutError";
|
|
37822
|
+
if (!timedOut) throw err;
|
|
37823
|
+
}
|
|
37824
|
+
const duration_ms = Date.now() - started;
|
|
37825
|
+
const body = res ? await res.text().catch(() => "") : "";
|
|
37826
|
+
const r = { ok: Boolean(res?.ok), status: timedOut ? "timeout" : res?.ok ? "ok" : "error", http_status: res?.status ?? 0, duration_ms, output_tail: body.slice(-2e3) };
|
|
37827
|
+
return { ok: r.ok, summary: describeRun(task, "en tu computadora", r), where, status: r.status, http_status: r.http_status, duration_ms: r.duration_ms, output_tail: r.output_tail, path: declared.path };
|
|
37828
|
+
});
|
|
37327
37829
|
const logout = guard(async () => {
|
|
37328
37830
|
await clearCredentials(cfg);
|
|
37329
37831
|
return { ok: true, summary: "Saliste de tu cuenta de Kigo en esta computadora." };
|
|
37330
37832
|
});
|
|
37331
|
-
return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, design_guide, logout };
|
|
37833
|
+
return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, design_guide, set_secret, list_secrets, delete_secret, run_task, logout };
|
|
37332
37834
|
}
|
|
37333
37835
|
var HELP_SUMMARY = "Sindri te ayuda a crear apps internas peque\xF1as para tu equipo en minutos, con datos de prueba y sin escribir c\xF3digo: t\xFA me cuentas qu\xE9 necesitas y yo la construyo. Puedes pedirme crear una app, verla en tu computadora antes de publicarla, publicarla, cambiarle el nombre o la direcci\xF3n, volver a una versi\xF3n anterior, compartirla en Slack, actualizar su plantilla o borrarla. Ten en cuenta: las apps trabajan con datos sint\xE9ticos, nunca reales; funcionan de lunes a viernes de 9:05 a 24:00, hora de la Ciudad de M\xE9xico; cada app la edita solo quien la cre\xF3; y lo que deba llegar a toda la empresa se pide a desarrollo. Tus apps y sus versiones est\xE1n en https://sindri.kigo.dev.";
|
|
37334
37836
|
async function moveDir(from, to) {
|
|
37335
|
-
await
|
|
37837
|
+
await fs10.mkdir(path10.dirname(to), { recursive: true });
|
|
37336
37838
|
try {
|
|
37337
|
-
await
|
|
37839
|
+
await fs10.rename(from, to);
|
|
37338
37840
|
} catch (err) {
|
|
37339
37841
|
if (err.code !== "EXDEV") throw err;
|
|
37340
|
-
await
|
|
37341
|
-
await
|
|
37842
|
+
await fs10.cp(from, to, { recursive: true });
|
|
37843
|
+
await fs10.rm(from, { recursive: true, force: true });
|
|
37342
37844
|
}
|
|
37343
37845
|
}
|
|
37344
37846
|
function stamp(d = /* @__PURE__ */ new Date()) {
|
|
@@ -37357,8 +37859,8 @@ function describeTemplateFiles(r) {
|
|
|
37357
37859
|
return parts.length ? ` ${parts.join(" ")}` : "";
|
|
37358
37860
|
}
|
|
37359
37861
|
async function prepareLocalDb(tc, dir, opts = {}) {
|
|
37360
|
-
const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
|
|
37361
|
-
const fresh = !await exists2(
|
|
37862
|
+
const env = { ...tc.env, KIGO_ENV: "local", KIGO_APP_NAME: path10.basename(dir), DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
|
|
37863
|
+
const fresh = !await exists2(path10.join(dir, ".sindri", "pglite"));
|
|
37362
37864
|
const migrate = await run(tc.pnpm, [...PNPM_ARGS, "db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
|
|
37363
37865
|
if (migrate.code !== 0) return { ok: false, detail: lastLine(migrate.stderr || migrate.stdout) };
|
|
37364
37866
|
if (opts.seed ?? fresh) {
|
|
@@ -37375,6 +37877,23 @@ async function localState(cfg, name) {
|
|
|
37375
37877
|
const hash2 = await treeHash(dir);
|
|
37376
37878
|
return { exists: true, path: dir, unpublished: !published || published.tree_hash !== hash2, published_version: published?.version ?? null };
|
|
37377
37879
|
}
|
|
37880
|
+
var LOCAL_TASK_TIMEOUT_MS = 5 * 6e4;
|
|
37881
|
+
function describeTasks(info) {
|
|
37882
|
+
const tasks = info.tasks ?? [];
|
|
37883
|
+
if (tasks.length === 0) return "";
|
|
37884
|
+
const one = (t) => {
|
|
37885
|
+
const last = t.last_run_at ? `\xFAltima ${when(t.last_run_at)}: ${STATUS_WORD[t.last_status ?? "ok"] ?? t.last_status}` : "a\xFAn no corre";
|
|
37886
|
+
return `${t.name} cada ${t.every} (${last})`;
|
|
37887
|
+
};
|
|
37888
|
+
return `${tasks.length === 1 ? "Tarea" : "Tareas"}: ${tasks.map(one).join("; ")}.`;
|
|
37889
|
+
}
|
|
37890
|
+
var STATUS_WORD = { ok: "bien", error: "fall\xF3", timeout: "se pas\xF3 de tiempo" };
|
|
37891
|
+
function describeRun(task, where, r) {
|
|
37892
|
+
const secs = (r.duration_ms / 1e3).toFixed(r.duration_ms < 1e4 ? 1 : 0).replace(".", ",");
|
|
37893
|
+
if (r.status === "ok") return `La tarea \xAB${task}\xBB corri\xF3 ${where} en ${secs} s y termin\xF3 bien.`;
|
|
37894
|
+
if (r.status === "timeout") return `La tarea \xAB${task}\xBB ${where} se pas\xF3 de tiempo (${secs} s) y se cort\xF3. Revisa qu\xE9 la detiene; el detalle est\xE1 en output_tail.`;
|
|
37895
|
+
return `La tarea \xAB${task}\xBB ${where} fall\xF3${r.http_status ? ` (HTTP ${r.http_status})` : ""} tras ${secs} s. Revisa el \xFAltimo cambio; el detalle est\xE1 en output_tail.`;
|
|
37896
|
+
}
|
|
37378
37897
|
function describeVersion(info) {
|
|
37379
37898
|
if (!info.last_version) return `"${info.name}" a\xFAn no tiene versiones publicadas; su direcci\xF3n ser\xE1 ${info.url}.`;
|
|
37380
37899
|
return `"${info.name}" est\xE1 en ${info.url}; \xFAltima versi\xF3n ${when(info.last_version.at)}, \xAB${info.last_version.label}\xBB.`;
|
|
@@ -37390,7 +37909,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
37390
37909
|
}
|
|
37391
37910
|
|
|
37392
37911
|
// src/index.ts
|
|
37393
|
-
var VERSION = "0.1.
|
|
37912
|
+
var VERSION = "0.1.6";
|
|
37394
37913
|
function registerTools(server, tools) {
|
|
37395
37914
|
for (const name of Object.keys(SCHEMAS)) {
|
|
37396
37915
|
const meta3 = DESCRIPTIONS[name];
|