@kigo-dev/sindri 0.1.4 → 0.1.5
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 +676 -172
- package/manifest.json +17 -1
- 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,42 @@ 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
|
}
|
|
36629
36767
|
async function installFnmLts(cfg, env) {
|
|
36630
|
-
await
|
|
36768
|
+
await fs7.mkdir(cfg.fnmDir, { recursive: true });
|
|
36631
36769
|
const script = await (await fetch("https://fnm.vercel.app/install")).text();
|
|
36632
36770
|
const sh = await run("bash", ["-s", "--", "--install-dir", cfg.fnmDir, "--skip-shell"], { env, input: script, timeoutMs: 3e5 });
|
|
36633
36771
|
if (sh.code !== 0) throw new Error(`fnm installer failed: ${sh.stderr.trim().slice(-300)}`);
|
|
36634
|
-
const fnm =
|
|
36772
|
+
const fnm = path7.join(cfg.fnmDir, "fnm");
|
|
36635
36773
|
const fnmEnv = { ...env, FNM_DIR: cfg.fnmDir };
|
|
36636
36774
|
const inst = await run(fnm, ["install", "--lts"], { env: fnmEnv, timeoutMs: 6e5 });
|
|
36637
36775
|
if (inst.code !== 0) throw new Error(`fnm install --lts failed: ${inst.stderr.trim().slice(-300)}`);
|
|
36638
36776
|
const def = await run(fnm, ["default", "lts-latest"], { env: fnmEnv, timeoutMs: 6e4 });
|
|
36639
36777
|
if (def.code !== 0) throw new Error(`fnm default failed: ${def.stderr.trim().slice(-300)}`);
|
|
36640
|
-
const bin =
|
|
36641
|
-
const childEnv = { ...env, PATH: `${bin}${
|
|
36642
|
-
const enable = await run(
|
|
36778
|
+
const bin = path7.join(cfg.fnmDir, "aliases", "default", "bin");
|
|
36779
|
+
const childEnv = { ...env, PATH: `${bin}${path7.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
|
|
36780
|
+
const enable = await run(path7.join(bin, "corepack"), ["enable", "--install-directory", bin], { env: childEnv, timeoutMs: 6e4 });
|
|
36643
36781
|
if (enable.code !== 0) throw new Error(`corepack enable failed: ${enable.stderr.trim().slice(-300)}`);
|
|
36644
|
-
const prep = await run(
|
|
36782
|
+
const prep = await run(path7.join(bin, "corepack"), ["prepare", `pnpm@${cfg.pnpmVersion}`, "--activate"], { env: childEnv, timeoutMs: 3e5 });
|
|
36645
36783
|
if (prep.code !== 0) throw new Error(`corepack prepare pnpm failed: ${prep.stderr.trim().slice(-300)}`);
|
|
36646
36784
|
}
|
|
36647
36785
|
|
|
36648
36786
|
// src/preview.ts
|
|
36649
36787
|
function registryFile(cfg) {
|
|
36650
|
-
return
|
|
36788
|
+
return path8.join(cfg.stateDir, "previews.json");
|
|
36651
36789
|
}
|
|
36652
36790
|
async function readRegistry(cfg) {
|
|
36653
36791
|
try {
|
|
36654
|
-
return JSON.parse(await
|
|
36792
|
+
return JSON.parse(await fs8.readFile(registryFile(cfg), "utf8"));
|
|
36655
36793
|
} catch {
|
|
36656
36794
|
return {};
|
|
36657
36795
|
}
|
|
36658
36796
|
}
|
|
36659
36797
|
async function writeRegistry(cfg, reg) {
|
|
36660
|
-
await
|
|
36661
|
-
await
|
|
36798
|
+
await fs8.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
|
|
36799
|
+
await fs8.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
|
|
36662
36800
|
}
|
|
36663
36801
|
async function freePort(from = 3e3, to = 3099) {
|
|
36664
36802
|
for (let p = from; p <= to; p++) {
|
|
@@ -36695,9 +36833,9 @@ async function startPreview(cfg, tc, app, dir, opts = {}) {
|
|
|
36695
36833
|
const existing = await runningPreview(cfg, app, fetchImpl);
|
|
36696
36834
|
if (existing) return { port: existing.port, url: `http://127.0.0.1:${existing.port}`, reused: true };
|
|
36697
36835
|
const port = await freePort();
|
|
36698
|
-
await
|
|
36699
|
-
const logFile =
|
|
36700
|
-
await
|
|
36836
|
+
await fs8.mkdir(path8.join(dir, ".sindri"), { recursive: true });
|
|
36837
|
+
const logFile = path8.join(dir, ".sindri", "preview.log");
|
|
36838
|
+
await fs8.writeFile(logFile, "");
|
|
36701
36839
|
const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", PORT: String(port), CI: "1", FORCE_COLOR: "0", BROWSER: "none" };
|
|
36702
36840
|
const pid = spawnDetached(tc.pnpm, [...PNPM_ARGS, "dev", "--port", String(port)], { cwd: dir, env, logFile });
|
|
36703
36841
|
const reg = await readRegistry(cfg);
|
|
@@ -36713,7 +36851,7 @@ async function startPreview(cfg, tc, app, dir, opts = {}) {
|
|
|
36713
36851
|
killTree(pid);
|
|
36714
36852
|
delete reg[app];
|
|
36715
36853
|
await writeRegistry(cfg, reg);
|
|
36716
|
-
const log = await
|
|
36854
|
+
const log = await fs8.readFile(logFile, "utf8").catch(() => "");
|
|
36717
36855
|
return { port, url: `http://127.0.0.1:${port}`, reused: false, log: log.split("\n").slice(-15).join("\n") };
|
|
36718
36856
|
}
|
|
36719
36857
|
async function stopPreview(cfg, app) {
|
|
@@ -36730,11 +36868,200 @@ async function openInBrowser(cfg, url2) {
|
|
|
36730
36868
|
await run(cfg.opener, [url2], { timeoutMs: 1e4 });
|
|
36731
36869
|
}
|
|
36732
36870
|
|
|
36871
|
+
// src/schedules.ts
|
|
36872
|
+
var SCHEDULE_NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;
|
|
36873
|
+
var SCHEDULE_EVERY = ["5m", "10m", "15m", "30m", "1h", "2h", "6h", "12h", "1d"];
|
|
36874
|
+
var SCHEDULES_MAX = 5;
|
|
36875
|
+
function readSchedules(manifestText) {
|
|
36876
|
+
const raw = manifestValue(manifestText, "schedules");
|
|
36877
|
+
if (raw === void 0 || raw.trim() === "") return { ok: true, tasks: [] };
|
|
36878
|
+
let parsed;
|
|
36879
|
+
try {
|
|
36880
|
+
parsed = JSON.parse(raw);
|
|
36881
|
+
} catch {
|
|
36882
|
+
return { ok: false, error: "La l\xEDnea schedules de kigo-app.yaml no es una lista JSON v\xE1lida." };
|
|
36883
|
+
}
|
|
36884
|
+
if (!Array.isArray(parsed)) return { ok: false, error: "La l\xEDnea schedules de kigo-app.yaml debe ser una lista JSON." };
|
|
36885
|
+
if (parsed.length > SCHEDULES_MAX) return { ok: false, error: `Una app puede declarar hasta ${SCHEDULES_MAX} tareas.` };
|
|
36886
|
+
const tasks = [];
|
|
36887
|
+
for (const item of parsed) {
|
|
36888
|
+
if (!item || typeof item !== "object") return { ok: false, error: "Cada tarea de schedules debe ser un objeto {name, path, every}." };
|
|
36889
|
+
const { name, path: path11, every } = item;
|
|
36890
|
+
if (typeof name !== "string" || !SCHEDULE_NAME_RE.test(name)) {
|
|
36891
|
+
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.` };
|
|
36892
|
+
}
|
|
36893
|
+
if (tasks.some((t) => t.name === name)) return { ok: false, error: `La tarea \xAB${name}\xBB est\xE1 declarada dos veces.` };
|
|
36894
|
+
if (typeof path11 !== "string" || !path11.startsWith("/api/") || path11.includes("?") || path11.includes("#")) {
|
|
36895
|
+
return { ok: false, error: `La ruta de la tarea \xAB${name}\xBB debe empezar con /api/ y no llevar par\xE1metros.` };
|
|
36896
|
+
}
|
|
36897
|
+
if (typeof every !== "string" || !SCHEDULE_EVERY.includes(every)) {
|
|
36898
|
+
return { ok: false, error: `La frecuencia de la tarea \xAB${name}\xBB debe ser una de: ${SCHEDULE_EVERY.join(", ")}.` };
|
|
36899
|
+
}
|
|
36900
|
+
tasks.push({ name, path: path11, every });
|
|
36901
|
+
}
|
|
36902
|
+
return { ok: true, tasks };
|
|
36903
|
+
}
|
|
36904
|
+
|
|
36905
|
+
// src/secretpage.ts
|
|
36906
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
36907
|
+
import http2 from "node:http";
|
|
36908
|
+
var SECRET_MAX_BYTES = 8 * 1024;
|
|
36909
|
+
var SecretPageBrowserUnavailable = class extends Error {
|
|
36910
|
+
constructor() {
|
|
36911
|
+
super("browser unavailable");
|
|
36912
|
+
}
|
|
36913
|
+
};
|
|
36914
|
+
async function collectSecret(opts) {
|
|
36915
|
+
const nonce = randomBytes3(16).toString("base64url");
|
|
36916
|
+
const server = http2.createServer();
|
|
36917
|
+
await new Promise((resolve, reject) => {
|
|
36918
|
+
server.once("error", reject);
|
|
36919
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
36920
|
+
});
|
|
36921
|
+
const port = server.address().port;
|
|
36922
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
36923
|
+
const route = `/s/${nonce}`;
|
|
36924
|
+
const url2 = `${origin}${route}`;
|
|
36925
|
+
let timer;
|
|
36926
|
+
let settled = null;
|
|
36927
|
+
let used = false;
|
|
36928
|
+
const done = new Promise((res0, rej0) => {
|
|
36929
|
+
const resolve = () => {
|
|
36930
|
+
settled = { ok: true };
|
|
36931
|
+
res0();
|
|
36932
|
+
};
|
|
36933
|
+
const reject = (e) => {
|
|
36934
|
+
settled = { err: e };
|
|
36935
|
+
rej0(e);
|
|
36936
|
+
};
|
|
36937
|
+
timer = setTimeout(() => reject(new Error("expired")), opts.timeoutMs ?? 5 * 6e4);
|
|
36938
|
+
server.on("request", (req, res) => {
|
|
36939
|
+
const html = (status, body) => {
|
|
36940
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Frame-Options": "DENY" });
|
|
36941
|
+
res.end(body);
|
|
36942
|
+
};
|
|
36943
|
+
const u = new URL(req.url ?? "/", origin);
|
|
36944
|
+
if (u.pathname !== route || used) {
|
|
36945
|
+
html(404, PAGE("Esta p\xE1gina ya no est\xE1 disponible.", "Vuelve a Claude y pide guardar el secreto otra vez."));
|
|
36946
|
+
return;
|
|
36947
|
+
}
|
|
36948
|
+
if (req.method === "GET") {
|
|
36949
|
+
html(200, FORM_PAGE(opts.app, opts.name, opts.hint));
|
|
36950
|
+
return;
|
|
36951
|
+
}
|
|
36952
|
+
if (req.method !== "POST") {
|
|
36953
|
+
res.writeHead(405, { Allow: "GET, POST" }).end();
|
|
36954
|
+
return;
|
|
36955
|
+
}
|
|
36956
|
+
const from = req.headers.origin;
|
|
36957
|
+
if (from && from !== origin) {
|
|
36958
|
+
html(403, PAGE("Esa petici\xF3n no viene de esta p\xE1gina.", "Vuelve a Claude y pide guardar el secreto otra vez."));
|
|
36959
|
+
return;
|
|
36960
|
+
}
|
|
36961
|
+
const chunks = [];
|
|
36962
|
+
let size = 0;
|
|
36963
|
+
let tooBig = false;
|
|
36964
|
+
req.on("data", (c) => {
|
|
36965
|
+
size += c.length;
|
|
36966
|
+
if (size > SECRET_MAX_BYTES * 3 + 1024) {
|
|
36967
|
+
tooBig = true;
|
|
36968
|
+
req.destroy();
|
|
36969
|
+
return;
|
|
36970
|
+
}
|
|
36971
|
+
chunks.push(c);
|
|
36972
|
+
});
|
|
36973
|
+
req.on("end", () => {
|
|
36974
|
+
if (tooBig) return;
|
|
36975
|
+
const body = new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
|
|
36976
|
+
let value = body.get("value") ?? "";
|
|
36977
|
+
value = value.replace(/\r?\n$/, "");
|
|
36978
|
+
if (value === "") {
|
|
36979
|
+
html(400, FORM_PAGE(opts.app, opts.name, opts.hint, "Pega el valor antes de guardar."));
|
|
36980
|
+
return;
|
|
36981
|
+
}
|
|
36982
|
+
if (Buffer.byteLength(value, "utf8") > SECRET_MAX_BYTES) {
|
|
36983
|
+
html(400, FORM_PAGE(opts.app, opts.name, opts.hint, "El valor es demasiado largo (m\xE1ximo 8 KB)."));
|
|
36984
|
+
return;
|
|
36985
|
+
}
|
|
36986
|
+
used = true;
|
|
36987
|
+
opts.onValue(value).then(() => {
|
|
36988
|
+
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.`));
|
|
36989
|
+
clearTimeout(timer);
|
|
36990
|
+
resolve();
|
|
36991
|
+
}).catch((err) => {
|
|
36992
|
+
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."));
|
|
36993
|
+
clearTimeout(timer);
|
|
36994
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
36995
|
+
});
|
|
36996
|
+
});
|
|
36997
|
+
});
|
|
36998
|
+
});
|
|
36999
|
+
done.catch(() => void 0);
|
|
37000
|
+
try {
|
|
37001
|
+
if (!await opts.openUrl(url2)) {
|
|
37002
|
+
const state0 = settled;
|
|
37003
|
+
if (state0?.err) throw state0.err;
|
|
37004
|
+
if (!state0) throw new SecretPageBrowserUnavailable();
|
|
37005
|
+
}
|
|
37006
|
+
await done;
|
|
37007
|
+
return { url: url2 };
|
|
37008
|
+
} finally {
|
|
37009
|
+
clearTimeout(timer);
|
|
37010
|
+
server.close();
|
|
37011
|
+
server.closeAllConnections?.();
|
|
37012
|
+
}
|
|
37013
|
+
}
|
|
37014
|
+
function escapeHtml(s) {
|
|
37015
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
37016
|
+
}
|
|
37017
|
+
var STYLE = `
|
|
37018
|
+
:root{--primary:#FF6900;--bg:#FEF9F8;--card:#fff;--fg:#0f172a;--muted:#64748b;--border:#e7e0dc;--danger:#b91c1c}
|
|
37019
|
+
*{box-sizing:border-box}
|
|
37020
|
+
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}
|
|
37021
|
+
main{width:min(30rem,calc(100vw - 2rem));background:var(--card);border:1px solid var(--border);border-radius:20px;padding:2rem}
|
|
37022
|
+
.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}
|
|
37023
|
+
.brand i{display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--primary)}
|
|
37024
|
+
h1{font-size:20px;margin:0 0 .5rem;font-weight:600}
|
|
37025
|
+
p{margin:0 0 1rem;color:var(--muted)}
|
|
37026
|
+
code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#f5efec;padding:.1rem .4rem;border-radius:6px;color:var(--fg)}
|
|
37027
|
+
label{display:block;font-size:13px;font-weight:600;margin:1rem 0 .35rem}
|
|
37028
|
+
.field{display:flex;gap:.5rem}
|
|
37029
|
+
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)}
|
|
37030
|
+
input:focus-visible,button:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
|
|
37031
|
+
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}
|
|
37032
|
+
button.primary{background:var(--primary);border-color:var(--primary);color:#fff;width:100%;margin-top:1.25rem}
|
|
37033
|
+
.error{color:var(--danger);font-size:13px;margin:.5rem 0 0}
|
|
37034
|
+
.note{font-size:13px;margin-top:1rem}
|
|
37035
|
+
`;
|
|
37036
|
+
function shell(title, body) {
|
|
37037
|
+
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>`;
|
|
37038
|
+
}
|
|
37039
|
+
function FORM_PAGE(app, name, hint, error61) {
|
|
37040
|
+
const title = `Guardar secreto para \xAB${app}\xBB`;
|
|
37041
|
+
return shell(
|
|
37042
|
+
title,
|
|
37043
|
+
`<h1>${escapeHtml(title)}</h1>
|
|
37044
|
+
<p>Clave: <code>${escapeHtml(name)}</code></p>
|
|
37045
|
+
${hint ? `<p>${escapeHtml(hint)}</p>` : ""}
|
|
37046
|
+
<form method="post" autocomplete="off">
|
|
37047
|
+
<label for="value">Valor</label>
|
|
37048
|
+
<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>
|
|
37049
|
+
${error61 ? `<p class="error" role="alert">${escapeHtml(error61)}</p>` : ""}
|
|
37050
|
+
<button type="submit" class="primary">Guardar</button>
|
|
37051
|
+
</form>
|
|
37052
|
+
<p class="note">Se guarda en tu computadora y en Sindri. Claude no lo ve ni lo repite.</p>
|
|
37053
|
+
<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>`
|
|
37054
|
+
);
|
|
37055
|
+
}
|
|
37056
|
+
function PAGE(title, text) {
|
|
37057
|
+
return shell(title, `<h1>${escapeHtml(title)}</h1><p>${text}</p>`);
|
|
37058
|
+
}
|
|
37059
|
+
|
|
36733
37060
|
// src/tree.ts
|
|
36734
37061
|
import { createHash as createHash2 } from "node:crypto";
|
|
36735
|
-
import
|
|
37062
|
+
import fs9 from "node:fs/promises";
|
|
36736
37063
|
import os3 from "node:os";
|
|
36737
|
-
import
|
|
37064
|
+
import path9 from "node:path";
|
|
36738
37065
|
var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".sindri", "dist", ".turbo", ".git"]);
|
|
36739
37066
|
function excluded(name) {
|
|
36740
37067
|
if (EXCLUDED_DIRS.has(name)) return true;
|
|
@@ -36745,11 +37072,11 @@ function excluded(name) {
|
|
|
36745
37072
|
async function listFiles(root) {
|
|
36746
37073
|
const out = [];
|
|
36747
37074
|
async function walk(dir, rel) {
|
|
36748
|
-
const entries = await
|
|
37075
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
36749
37076
|
for (const e of entries) {
|
|
36750
37077
|
if (excluded(e.name)) continue;
|
|
36751
37078
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
36752
|
-
if (e.isDirectory()) await walk(
|
|
37079
|
+
if (e.isDirectory()) await walk(path9.join(dir, e.name), r);
|
|
36753
37080
|
else if (e.isFile()) out.push(r);
|
|
36754
37081
|
}
|
|
36755
37082
|
}
|
|
@@ -36760,7 +37087,7 @@ async function treeHash(root) {
|
|
|
36760
37087
|
const files = await listFiles(root);
|
|
36761
37088
|
const h = createHash2("sha256");
|
|
36762
37089
|
for (const rel of files) {
|
|
36763
|
-
const fh = createHash2("sha256").update(await
|
|
37090
|
+
const fh = createHash2("sha256").update(await fs9.readFile(path9.join(root, rel))).digest("hex");
|
|
36764
37091
|
h.update(`${rel}\0${fh}
|
|
36765
37092
|
`);
|
|
36766
37093
|
}
|
|
@@ -36769,49 +37096,49 @@ async function treeHash(root) {
|
|
|
36769
37096
|
async function packTree(root) {
|
|
36770
37097
|
const files = await listFiles(root);
|
|
36771
37098
|
if (files.length === 0) throw new Error("empty tree");
|
|
36772
|
-
const listFile =
|
|
36773
|
-
await
|
|
36774
|
-
const out =
|
|
37099
|
+
const listFile = path9.join(await fs9.mkdtemp(path9.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
|
|
37100
|
+
await fs9.writeFile(listFile, files.join("\n") + "\n");
|
|
37101
|
+
const out = path9.join(path9.dirname(listFile), "app.tar.gz");
|
|
36775
37102
|
const r = await run("tar", ["-czf", out, "-C", root, "-T", listFile], { timeoutMs: 12e4 });
|
|
36776
37103
|
if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
|
|
36777
|
-
const st = await
|
|
37104
|
+
const st = await fs9.stat(out);
|
|
36778
37105
|
return { file: out, files: files.length, bytes: st.size };
|
|
36779
37106
|
}
|
|
36780
37107
|
async function replaceTree(root, archive) {
|
|
36781
|
-
const tmp = await
|
|
37108
|
+
const tmp = await fs9.mkdtemp(path9.join(os3.tmpdir(), "sindri-unpack-"));
|
|
36782
37109
|
const r = await run("tar", ["-xzf", archive, "-C", tmp], { timeoutMs: 12e4 });
|
|
36783
37110
|
if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
|
|
36784
37111
|
const src = await singleRoot(tmp);
|
|
36785
|
-
await
|
|
37112
|
+
await fs9.mkdir(root, { recursive: true });
|
|
36786
37113
|
await prune(root, 0);
|
|
36787
|
-
await
|
|
36788
|
-
await
|
|
37114
|
+
await fs9.cp(src, root, { recursive: true, force: true, filter: (s) => path9.basename(s) !== "node_modules" && path9.basename(s) !== ".DS_Store" });
|
|
37115
|
+
await fs9.rm(tmp, { recursive: true, force: true });
|
|
36789
37116
|
}
|
|
36790
37117
|
async function prune(dir, depth) {
|
|
36791
|
-
const entries = await
|
|
37118
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
36792
37119
|
for (const e of entries) {
|
|
36793
|
-
const p =
|
|
37120
|
+
const p = path9.join(dir, e.name);
|
|
36794
37121
|
if (e.name === "node_modules") continue;
|
|
36795
37122
|
if (depth === 3 && (e.name === ".sindri" || /^\.env(\..*)?$/.test(e.name))) continue;
|
|
36796
37123
|
if (e.isDirectory()) {
|
|
36797
37124
|
await prune(p, depth + 1);
|
|
36798
|
-
const left = await
|
|
36799
|
-
if (left.length === 0) await
|
|
37125
|
+
const left = await fs9.readdir(p);
|
|
37126
|
+
if (left.length === 0) await fs9.rmdir(p);
|
|
36800
37127
|
} else {
|
|
36801
|
-
await
|
|
37128
|
+
await fs9.rm(p, { force: true });
|
|
36802
37129
|
}
|
|
36803
37130
|
}
|
|
36804
37131
|
}
|
|
36805
37132
|
async function singleRoot(dir) {
|
|
36806
|
-
const entries = (await
|
|
36807
|
-
if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(
|
|
36808
|
-
return
|
|
37133
|
+
const entries = (await fs9.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
|
|
37134
|
+
if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path9.join(dir, "package.json"))) {
|
|
37135
|
+
return path9.join(dir, entries[0].name);
|
|
36809
37136
|
}
|
|
36810
37137
|
return dir;
|
|
36811
37138
|
}
|
|
36812
37139
|
async function exists2(p) {
|
|
36813
37140
|
try {
|
|
36814
|
-
await
|
|
37141
|
+
await fs9.access(p);
|
|
36815
37142
|
return true;
|
|
36816
37143
|
} catch {
|
|
36817
37144
|
return false;
|
|
@@ -36819,14 +37146,14 @@ async function exists2(p) {
|
|
|
36819
37146
|
}
|
|
36820
37147
|
async function readPublished(root) {
|
|
36821
37148
|
try {
|
|
36822
|
-
return JSON.parse(await
|
|
37149
|
+
return JSON.parse(await fs9.readFile(path9.join(root, ".sindri", "last-published.json"), "utf8"));
|
|
36823
37150
|
} catch {
|
|
36824
37151
|
return null;
|
|
36825
37152
|
}
|
|
36826
37153
|
}
|
|
36827
37154
|
async function writePublished(root, p) {
|
|
36828
|
-
await
|
|
36829
|
-
await
|
|
37155
|
+
await fs9.mkdir(path9.join(root, ".sindri"), { recursive: true });
|
|
37156
|
+
await fs9.writeFile(path9.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
|
|
36830
37157
|
}
|
|
36831
37158
|
|
|
36832
37159
|
// src/tools.ts
|
|
@@ -36859,7 +37186,7 @@ var SCHEMAS = {
|
|
|
36859
37186
|
preview: { app: appName },
|
|
36860
37187
|
publish: {
|
|
36861
37188
|
app: appName,
|
|
36862
|
-
message: external_exports.string().min(3).max(MESSAGE_MAX).describe("What changed, in Spanish, as the
|
|
37189
|
+
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
37190
|
},
|
|
36864
37191
|
status: { app: appName },
|
|
36865
37192
|
list_apps: {},
|
|
@@ -36869,27 +37196,42 @@ var SCHEMAS = {
|
|
|
36869
37196
|
rename_app: {
|
|
36870
37197
|
app: appName,
|
|
36871
37198
|
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
|
|
37199
|
+
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
37200
|
},
|
|
36874
37201
|
delete_app: {
|
|
36875
37202
|
app: appName,
|
|
36876
|
-
confirm: external_exports.string().describe("The app name exactly as the
|
|
37203
|
+
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
37204
|
},
|
|
36878
37205
|
share: { app: appName },
|
|
36879
37206
|
help: {},
|
|
36880
37207
|
design_guide: {
|
|
36881
37208
|
app: appName,
|
|
36882
37209
|
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.")
|
|
37210
|
+
},
|
|
37211
|
+
set_secret: {
|
|
37212
|
+
app: appName,
|
|
37213
|
+
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."),
|
|
37214
|
+
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).")
|
|
37215
|
+
},
|
|
37216
|
+
list_secrets: { app: appName },
|
|
37217
|
+
delete_secret: {
|
|
37218
|
+
app: appName,
|
|
37219
|
+
name: external_exports.string().min(2).max(64).describe("The secret's name, as listed by list_secrets.")
|
|
37220
|
+
},
|
|
37221
|
+
run_task: {
|
|
37222
|
+
app: appName,
|
|
37223
|
+
task: external_exports.string().min(2).max(32).describe("Task name as declared in the `schedules` line of kigo-app.yaml."),
|
|
37224
|
+
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
37225
|
}
|
|
36884
37226
|
};
|
|
36885
37227
|
var DESCRIPTIONS = {
|
|
36886
37228
|
setup: {
|
|
36887
37229
|
title: "Set up this computer",
|
|
36888
|
-
description: "Installs what Sindri needs on the
|
|
37230
|
+
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
37231
|
},
|
|
36890
37232
|
login: {
|
|
36891
37233
|
title: "Sign in to Kigo",
|
|
36892
|
-
description: "Signs the
|
|
37234
|
+
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
37235
|
},
|
|
36894
37236
|
create_app: {
|
|
36895
37237
|
title: "Create an app",
|
|
@@ -36897,7 +37239,7 @@ var DESCRIPTIONS = {
|
|
|
36897
37239
|
},
|
|
36898
37240
|
preview: {
|
|
36899
37241
|
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
|
|
37242
|
+
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
37243
|
},
|
|
36902
37244
|
publish: {
|
|
36903
37245
|
title: "Publish to Sindri",
|
|
@@ -36907,7 +37249,7 @@ var DESCRIPTIONS = {
|
|
|
36907
37249
|
title: "App status",
|
|
36908
37250
|
description: "Address, last published version, whether the local folder exists and whether it has unpublished changes."
|
|
36909
37251
|
},
|
|
36910
|
-
list_apps: { title: "List apps", description: "The
|
|
37252
|
+
list_apps: { title: "List apps", description: "The builder's apps in Sindri, with address, last version and whether each has a local folder." },
|
|
36911
37253
|
versions: { title: "Versions", description: "Published versions of an app, most recent first (id, date, label)." },
|
|
36912
37254
|
rollback: {
|
|
36913
37255
|
title: "Go back to a version",
|
|
@@ -36915,24 +37257,40 @@ var DESCRIPTIONS = {
|
|
|
36915
37257
|
},
|
|
36916
37258
|
update_template: {
|
|
36917
37259
|
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
|
|
37260
|
+
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
37261
|
},
|
|
36920
37262
|
rename_app: {
|
|
36921
37263
|
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
|
|
37264
|
+
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
37265
|
},
|
|
36924
37266
|
delete_app: {
|
|
36925
37267
|
title: "Delete an app",
|
|
36926
|
-
description: "Removes the app from Sindri (address, published versions and its database) after the
|
|
37268
|
+
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
37269
|
},
|
|
36928
37270
|
share: { title: "Share", description: "The line to paste in Slack: address, test-data notice and how the team enters." },
|
|
36929
37271
|
help: {
|
|
36930
37272
|
title: "What Sindri is",
|
|
36931
|
-
description: "No network. The Spanish introduction to give the
|
|
37273
|
+
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
37274
|
},
|
|
36933
37275
|
design_guide: {
|
|
36934
37276
|
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
|
|
37277
|
+
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."
|
|
37278
|
+
},
|
|
37279
|
+
set_secret: {
|
|
37280
|
+
title: "Save a secret",
|
|
37281
|
+
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."
|
|
37282
|
+
},
|
|
37283
|
+
list_secrets: {
|
|
37284
|
+
title: "List secrets",
|
|
37285
|
+
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."
|
|
37286
|
+
},
|
|
37287
|
+
delete_secret: {
|
|
37288
|
+
title: "Delete a secret",
|
|
37289
|
+
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."
|
|
37290
|
+
},
|
|
37291
|
+
run_task: {
|
|
37292
|
+
title: "Run a scheduled task now",
|
|
37293
|
+
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
37294
|
}
|
|
36937
37295
|
};
|
|
36938
37296
|
function createTools(deps) {
|
|
@@ -37027,14 +37385,14 @@ function createTools(deps) {
|
|
|
37027
37385
|
}
|
|
37028
37386
|
const created = await api.createApp(a.name, a.description.trim());
|
|
37029
37387
|
const archive = await api.download(created.archive_url);
|
|
37030
|
-
await
|
|
37388
|
+
await fs10.mkdir(p.root, { recursive: true });
|
|
37031
37389
|
await replaceTree(p.root, archive);
|
|
37032
|
-
await
|
|
37390
|
+
await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
|
|
37033
37391
|
p = await appPaths(cfg, a.name, me.owner);
|
|
37034
37392
|
if (!await appPresent(p)) {
|
|
37035
37393
|
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
37394
|
}
|
|
37037
|
-
await
|
|
37395
|
+
await ensureLocalEnv(p.appDir, a.name);
|
|
37038
37396
|
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
37397
|
if (install.code !== 0) {
|
|
37040
37398
|
const why = lastLine(install.stderr || install.stdout);
|
|
@@ -37064,7 +37422,7 @@ function createTools(deps) {
|
|
|
37064
37422
|
}
|
|
37065
37423
|
const tc = await toolchain();
|
|
37066
37424
|
if (!tc) return NEEDS_SETUP;
|
|
37067
|
-
if (!await exists2(
|
|
37425
|
+
if (!await exists2(path10.join(p.root, "node_modules"))) {
|
|
37068
37426
|
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
37427
|
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
37428
|
}
|
|
@@ -37105,14 +37463,14 @@ function createTools(deps) {
|
|
|
37105
37463
|
}
|
|
37106
37464
|
const packed = await packTree(dir);
|
|
37107
37465
|
if (packed.bytes > 20 * 1024 * 1024) {
|
|
37108
|
-
await
|
|
37466
|
+
await fs10.rm(path10.dirname(packed.file), { recursive: true, force: true });
|
|
37109
37467
|
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
37468
|
}
|
|
37111
37469
|
let result;
|
|
37112
37470
|
try {
|
|
37113
37471
|
result = await api.publish(a.app, packed.file, message);
|
|
37114
37472
|
} finally {
|
|
37115
|
-
await
|
|
37473
|
+
await fs10.rm(path10.dirname(packed.file), { recursive: true, force: true });
|
|
37116
37474
|
}
|
|
37117
37475
|
await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
|
|
37118
37476
|
return {
|
|
@@ -37131,6 +37489,8 @@ function createTools(deps) {
|
|
|
37131
37489
|
if (!local.exists) parts.push("En esta computadora no est\xE1 su carpeta.");
|
|
37132
37490
|
else if (local.unpublished) parts.push("Tienes cambios en tu computadora que a\xFAn no publicas.");
|
|
37133
37491
|
else parts.push("Tu carpeta est\xE1 al d\xEDa con lo publicado.");
|
|
37492
|
+
const tasks = describeTasks(info);
|
|
37493
|
+
if (tasks) parts.push(tasks);
|
|
37134
37494
|
return { ok: true, summary: parts.join(" "), app: info, local };
|
|
37135
37495
|
});
|
|
37136
37496
|
const list_apps = guard(async () => {
|
|
@@ -37154,16 +37514,16 @@ function createTools(deps) {
|
|
|
37154
37514
|
let p = await appPaths(cfg, a.app);
|
|
37155
37515
|
await stopPreview(cfg, a.app);
|
|
37156
37516
|
const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version.id);
|
|
37157
|
-
await
|
|
37517
|
+
await fs10.mkdir(p.root, { recursive: true });
|
|
37158
37518
|
await replaceTree(p.root, archive);
|
|
37159
|
-
await
|
|
37519
|
+
await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
|
|
37160
37520
|
p = await appPaths(cfg, a.app, p.owner || void 0);
|
|
37161
37521
|
const dir = p.appDir;
|
|
37162
37522
|
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
|
-
|
|
37523
|
+
await ensureLocalEnv(dir, a.app);
|
|
37164
37524
|
await linkDesignGuide(p);
|
|
37165
37525
|
const tc = await toolchain();
|
|
37166
|
-
if (tc && await exists2(
|
|
37526
|
+
if (tc && await exists2(path10.join(p.root, "node_modules"))) {
|
|
37167
37527
|
const db = await prepareLocalDb(tc, dir);
|
|
37168
37528
|
if (!db.ok) process.stderr.write(`sindri: local db after rollback: ${db.detail}
|
|
37169
37529
|
`);
|
|
@@ -37193,14 +37553,14 @@ function createTools(deps) {
|
|
|
37193
37553
|
await stopPreview(cfg, a.app);
|
|
37194
37554
|
const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version?.id);
|
|
37195
37555
|
await replaceTree(p.root, archive);
|
|
37196
|
-
await
|
|
37556
|
+
await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
|
|
37197
37557
|
p = await appPaths(cfg, a.app, p.owner || void 0);
|
|
37198
37558
|
const dir = p.appDir;
|
|
37199
37559
|
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
|
-
|
|
37560
|
+
await ensureLocalEnv(dir, a.app);
|
|
37201
37561
|
await linkDesignGuide(p);
|
|
37202
37562
|
const tc = await toolchain();
|
|
37203
|
-
if (tc && await exists2(
|
|
37563
|
+
if (tc && await exists2(path10.join(p.root, "node_modules"))) {
|
|
37204
37564
|
const db = await prepareLocalDb(tc, dir);
|
|
37205
37565
|
if (!db.ok) process.stderr.write(`sindri: local db after update_template: ${db.detail}
|
|
37206
37566
|
`);
|
|
@@ -37252,9 +37612,10 @@ function createTools(deps) {
|
|
|
37252
37612
|
await stopPreview(cfg, a.app);
|
|
37253
37613
|
const newRoot = appRoot(cfg, newName);
|
|
37254
37614
|
await moveDir(p.root, newRoot);
|
|
37255
|
-
const newDir =
|
|
37256
|
-
await moveDir(
|
|
37615
|
+
const newDir = path10.join(newRoot, "apps", p.owner, newName);
|
|
37616
|
+
await moveDir(path10.join(newRoot, "apps", p.owner, a.app), newDir);
|
|
37257
37617
|
await writeManifestValue(newDir, "name", newName);
|
|
37618
|
+
await writeEnvFile(newDir, upsertEnv(await readEnvFile(newDir), "KIGO_APP_NAME", newName));
|
|
37258
37619
|
await writePublished(newDir, { version: result.version ?? before.published_version, tree_hash: await treeHash(newDir), url: result.url });
|
|
37259
37620
|
await linkDesignGuide({ root: newRoot, appDir: newDir, owner: p.owner });
|
|
37260
37621
|
data.path = newDir;
|
|
@@ -37281,7 +37642,7 @@ function createTools(deps) {
|
|
|
37281
37642
|
}
|
|
37282
37643
|
let trash = null;
|
|
37283
37644
|
if (present) {
|
|
37284
|
-
trash =
|
|
37645
|
+
trash = path10.join(cfg.home, ".papelera", `${a.app}-${stamp()}`);
|
|
37285
37646
|
await moveDir(root, trash);
|
|
37286
37647
|
}
|
|
37287
37648
|
const kept = "Una copia de sus archivos queda en tu computadora, en la papelera de Sindri; puedes borrarla a mano cuando quieras.";
|
|
@@ -37324,21 +37685,147 @@ function createTools(deps) {
|
|
|
37324
37685
|
content: guide.content
|
|
37325
37686
|
};
|
|
37326
37687
|
});
|
|
37688
|
+
async function restartPreview(app, dir) {
|
|
37689
|
+
if (!await runningPreview(cfg, app, fetchImpl)) return false;
|
|
37690
|
+
const tc = await toolchain();
|
|
37691
|
+
if (!tc) return false;
|
|
37692
|
+
await stopPreview(cfg, app);
|
|
37693
|
+
const r = await startPreview(cfg, tc, app, dir, { fetchImpl, ...deps.sleepImpl ? { sleepImpl: deps.sleepImpl } : {} });
|
|
37694
|
+
return r.log === void 0;
|
|
37695
|
+
}
|
|
37696
|
+
function checkSecretName(name) {
|
|
37697
|
+
if (!validSecretName(name)) {
|
|
37698
|
+
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" };
|
|
37699
|
+
}
|
|
37700
|
+
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" };
|
|
37701
|
+
return null;
|
|
37702
|
+
}
|
|
37703
|
+
const set_secret = guard(async (a) => {
|
|
37704
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37705
|
+
const name = a.name.trim();
|
|
37706
|
+
const bad = checkSecretName(name);
|
|
37707
|
+
if (bad) return bad;
|
|
37708
|
+
await api.app(a.app);
|
|
37709
|
+
const p = await appPaths(cfg, a.app);
|
|
37710
|
+
const local = await appPresent(p);
|
|
37711
|
+
const openUrl = deps.openUrl ?? defaultOpenUrl(cfg);
|
|
37712
|
+
let restarted = false;
|
|
37713
|
+
try {
|
|
37714
|
+
await collectSecret({
|
|
37715
|
+
app: a.app,
|
|
37716
|
+
name,
|
|
37717
|
+
...a.hint ? { hint: a.hint.trim() } : {},
|
|
37718
|
+
openUrl,
|
|
37719
|
+
...deps.secretTimeoutMs ? { timeoutMs: deps.secretTimeoutMs } : {},
|
|
37720
|
+
onValue: async (value) => {
|
|
37721
|
+
if (local) await writeEnvFile(p.appDir, upsertEnv(await readEnvFile(p.appDir), name, value));
|
|
37722
|
+
restarted = (await api.putSecret(a.app, name, value)).restarted;
|
|
37723
|
+
}
|
|
37724
|
+
});
|
|
37725
|
+
} catch (err) {
|
|
37726
|
+
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" };
|
|
37727
|
+
if (err instanceof Error && err.message === "expired") {
|
|
37728
|
+
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 };
|
|
37729
|
+
}
|
|
37730
|
+
throw err;
|
|
37731
|
+
}
|
|
37732
|
+
const previewRestarted = local ? await restartPreview(a.app, p.appDir) : false;
|
|
37733
|
+
const where = local ? "en tu computadora y en Sindri" : "en Sindri (en esta computadora no est\xE1 la carpeta de la app)";
|
|
37734
|
+
const applied = previewRestarted ? " Reinici\xE9 la vista previa para que lo tome." : "";
|
|
37735
|
+
return { ok: true, summary: `Listo, ${name} qued\xF3 guardado ${where}.${applied}`, name, restarted, preview_restarted: previewRestarted, local };
|
|
37736
|
+
});
|
|
37737
|
+
const list_secrets = guard(async (a) => {
|
|
37738
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37739
|
+
const names = (await api.listSecrets(a.app)).sort();
|
|
37740
|
+
const p = await appPaths(cfg, a.app);
|
|
37741
|
+
const localNames = await appPresent(p) ? secretKeys(await readEnvFile(p.appDir)).sort() : [];
|
|
37742
|
+
const missingLocal = names.filter((n) => !localNames.includes(n));
|
|
37743
|
+
const missingInSindri = localNames.filter((n) => !names.includes(n));
|
|
37744
|
+
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(", ")}.`];
|
|
37745
|
+
if (missingLocal.length) parts.push(`En tu computadora ${missingLocal.length === 1 ? "falta" : "faltan"} ${missingLocal.join(", ")}.`);
|
|
37746
|
+
if (missingInSindri.length) parts.push(`En Sindri ${missingInSindri.length === 1 ? "falta" : "faltan"} ${missingInSindri.join(", ")} (solo est\xE1 en tu computadora).`);
|
|
37747
|
+
return { ok: true, summary: parts.join(" "), names, local_names: localNames, missing_local: missingLocal, missing_in_sindri: missingInSindri };
|
|
37748
|
+
});
|
|
37749
|
+
const delete_secret = guard(async (a) => {
|
|
37750
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37751
|
+
const name = a.name.trim();
|
|
37752
|
+
const bad = checkSecretName(name);
|
|
37753
|
+
if (bad) return bad;
|
|
37754
|
+
let inSindri = true;
|
|
37755
|
+
try {
|
|
37756
|
+
await api.deleteSecret(a.app, name);
|
|
37757
|
+
} catch (err) {
|
|
37758
|
+
if (err instanceof ApiError && err.status === 404) inSindri = false;
|
|
37759
|
+
else throw err;
|
|
37760
|
+
}
|
|
37761
|
+
const p = await appPaths(cfg, a.app);
|
|
37762
|
+
let hadLocal = false;
|
|
37763
|
+
if (await appPresent(p)) {
|
|
37764
|
+
const text = await readEnvFile(p.appDir);
|
|
37765
|
+
hadLocal = envValue(text, name) !== void 0;
|
|
37766
|
+
if (hadLocal) await writeEnvFile(p.appDir, removeEnv(text, name));
|
|
37767
|
+
}
|
|
37768
|
+
const previewRestarted = hadLocal ? await restartPreview(a.app, p.appDir) : false;
|
|
37769
|
+
const where = inSindri && hadLocal ? "de Sindri y de tu computadora" : inSindri ? "de Sindri" : hadLocal ? "de tu computadora (en Sindri no estaba)" : "";
|
|
37770
|
+
const summary = where ? `Borr\xE9 ${name} ${where}.${previewRestarted ? " Reinici\xE9 la vista previa." : ""}` : `${name} no estaba guardado ni en Sindri ni en tu computadora.`;
|
|
37771
|
+
return { ok: true, summary, name, removed_from_sindri: inSindri, removed_locally: hadLocal, preview_restarted: previewRestarted };
|
|
37772
|
+
});
|
|
37773
|
+
const run_task = guard(async (a) => {
|
|
37774
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37775
|
+
const task = a.task.trim();
|
|
37776
|
+
const p = await appPaths(cfg, a.app);
|
|
37777
|
+
const running = await runningPreview(cfg, a.app, fetchImpl);
|
|
37778
|
+
const where = a.where ?? (running ? "local" : "sindri");
|
|
37779
|
+
if (where === "sindri") {
|
|
37780
|
+
const r2 = await api.runTask(a.app, task);
|
|
37781
|
+
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 };
|
|
37782
|
+
}
|
|
37783
|
+
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" };
|
|
37784
|
+
const schedules = readSchedules(await readManifest(p.appDir));
|
|
37785
|
+
if (!schedules.ok) return { ok: false, summary: schedules.error, code: "bad_schedules" };
|
|
37786
|
+
const declared = schedules.tasks.find((t) => t.name === task);
|
|
37787
|
+
if (!declared) {
|
|
37788
|
+
const names = schedules.tasks.map((t) => t.name);
|
|
37789
|
+
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 };
|
|
37790
|
+
}
|
|
37791
|
+
const ensured = await ensureLocalEnv(p.appDir, a.app);
|
|
37792
|
+
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" };
|
|
37793
|
+
if (ensured.created_token) await restartPreview(a.app, p.appDir);
|
|
37794
|
+
const token = envValue(await readEnvFile(p.appDir), "SINDRI_TASK_TOKEN") ?? "";
|
|
37795
|
+
const port = (await runningPreview(cfg, a.app, fetchImpl))?.port ?? running.port;
|
|
37796
|
+
const started = Date.now();
|
|
37797
|
+
let res = null;
|
|
37798
|
+
let timedOut = false;
|
|
37799
|
+
try {
|
|
37800
|
+
res = await fetchImpl(`http://127.0.0.1:${port}${declared.path}`, {
|
|
37801
|
+
method: "POST",
|
|
37802
|
+
headers: { "X-Sindri-Task": task, Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
37803
|
+
signal: AbortSignal.timeout(LOCAL_TASK_TIMEOUT_MS)
|
|
37804
|
+
});
|
|
37805
|
+
} catch (err) {
|
|
37806
|
+
timedOut = err instanceof Error && err.name === "TimeoutError";
|
|
37807
|
+
if (!timedOut) throw err;
|
|
37808
|
+
}
|
|
37809
|
+
const duration_ms = Date.now() - started;
|
|
37810
|
+
const body = res ? await res.text().catch(() => "") : "";
|
|
37811
|
+
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) };
|
|
37812
|
+
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 };
|
|
37813
|
+
});
|
|
37327
37814
|
const logout = guard(async () => {
|
|
37328
37815
|
await clearCredentials(cfg);
|
|
37329
37816
|
return { ok: true, summary: "Saliste de tu cuenta de Kigo en esta computadora." };
|
|
37330
37817
|
});
|
|
37331
|
-
return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, design_guide, logout };
|
|
37818
|
+
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
37819
|
}
|
|
37333
37820
|
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
37821
|
async function moveDir(from, to) {
|
|
37335
|
-
await
|
|
37822
|
+
await fs10.mkdir(path10.dirname(to), { recursive: true });
|
|
37336
37823
|
try {
|
|
37337
|
-
await
|
|
37824
|
+
await fs10.rename(from, to);
|
|
37338
37825
|
} catch (err) {
|
|
37339
37826
|
if (err.code !== "EXDEV") throw err;
|
|
37340
|
-
await
|
|
37341
|
-
await
|
|
37827
|
+
await fs10.cp(from, to, { recursive: true });
|
|
37828
|
+
await fs10.rm(from, { recursive: true, force: true });
|
|
37342
37829
|
}
|
|
37343
37830
|
}
|
|
37344
37831
|
function stamp(d = /* @__PURE__ */ new Date()) {
|
|
@@ -37357,8 +37844,8 @@ function describeTemplateFiles(r) {
|
|
|
37357
37844
|
return parts.length ? ` ${parts.join(" ")}` : "";
|
|
37358
37845
|
}
|
|
37359
37846
|
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(
|
|
37847
|
+
const env = { ...tc.env, KIGO_ENV: "local", KIGO_APP_NAME: path10.basename(dir), DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
|
|
37848
|
+
const fresh = !await exists2(path10.join(dir, ".sindri", "pglite"));
|
|
37362
37849
|
const migrate = await run(tc.pnpm, [...PNPM_ARGS, "db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
|
|
37363
37850
|
if (migrate.code !== 0) return { ok: false, detail: lastLine(migrate.stderr || migrate.stdout) };
|
|
37364
37851
|
if (opts.seed ?? fresh) {
|
|
@@ -37375,6 +37862,23 @@ async function localState(cfg, name) {
|
|
|
37375
37862
|
const hash2 = await treeHash(dir);
|
|
37376
37863
|
return { exists: true, path: dir, unpublished: !published || published.tree_hash !== hash2, published_version: published?.version ?? null };
|
|
37377
37864
|
}
|
|
37865
|
+
var LOCAL_TASK_TIMEOUT_MS = 5 * 6e4;
|
|
37866
|
+
function describeTasks(info) {
|
|
37867
|
+
const tasks = info.tasks ?? [];
|
|
37868
|
+
if (tasks.length === 0) return "";
|
|
37869
|
+
const one = (t) => {
|
|
37870
|
+
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";
|
|
37871
|
+
return `${t.name} cada ${t.every} (${last})`;
|
|
37872
|
+
};
|
|
37873
|
+
return `${tasks.length === 1 ? "Tarea" : "Tareas"}: ${tasks.map(one).join("; ")}.`;
|
|
37874
|
+
}
|
|
37875
|
+
var STATUS_WORD = { ok: "bien", error: "fall\xF3", timeout: "se pas\xF3 de tiempo" };
|
|
37876
|
+
function describeRun(task, where, r) {
|
|
37877
|
+
const secs = (r.duration_ms / 1e3).toFixed(r.duration_ms < 1e4 ? 1 : 0).replace(".", ",");
|
|
37878
|
+
if (r.status === "ok") return `La tarea \xAB${task}\xBB corri\xF3 ${where} en ${secs} s y termin\xF3 bien.`;
|
|
37879
|
+
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.`;
|
|
37880
|
+
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.`;
|
|
37881
|
+
}
|
|
37378
37882
|
function describeVersion(info) {
|
|
37379
37883
|
if (!info.last_version) return `"${info.name}" a\xFAn no tiene versiones publicadas; su direcci\xF3n ser\xE1 ${info.url}.`;
|
|
37380
37884
|
return `"${info.name}" est\xE1 en ${info.url}; \xFAltima versi\xF3n ${when(info.last_version.at)}, \xAB${info.last_version.label}\xBB.`;
|
|
@@ -37390,7 +37894,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
37390
37894
|
}
|
|
37391
37895
|
|
|
37392
37896
|
// src/index.ts
|
|
37393
|
-
var VERSION = "0.1.
|
|
37897
|
+
var VERSION = "0.1.5";
|
|
37394
37898
|
function registerTools(server, tools) {
|
|
37395
37899
|
for (const name of Object.keys(SCHEMAS)) {
|
|
37396
37900
|
const meta3 = DESCRIPTIONS[name];
|