@abaigram/abaikit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -2
  3. package/abai/identity.md +1 -0
  4. package/abai/learnabai.md +474 -0
  5. package/abai/use.md +1 -0
  6. package/abaigram.yaml +208 -0
  7. package/dist/abaify.d.ts +68 -0
  8. package/dist/abaify.d.ts.map +1 -0
  9. package/dist/abaify.js +310 -0
  10. package/dist/abaify.js.map +1 -0
  11. package/dist/abaigram.d.ts +18 -0
  12. package/dist/abaigram.d.ts.map +1 -0
  13. package/dist/abaigram.js +153 -0
  14. package/dist/abaigram.js.map +1 -0
  15. package/dist/app.d.ts +19 -0
  16. package/dist/app.d.ts.map +1 -0
  17. package/dist/app.js +29 -0
  18. package/dist/app.js.map +1 -0
  19. package/dist/bin.d.ts +45 -0
  20. package/dist/bin.d.ts.map +1 -0
  21. package/dist/bin.js +274 -0
  22. package/dist/bin.js.map +1 -0
  23. package/dist/check.d.ts +119 -0
  24. package/dist/check.d.ts.map +1 -0
  25. package/dist/check.js +416 -0
  26. package/dist/check.js.map +1 -0
  27. package/dist/command.d.ts +41 -0
  28. package/dist/command.d.ts.map +1 -0
  29. package/dist/command.js +179 -0
  30. package/dist/command.js.map +1 -0
  31. package/dist/index.d.ts +5 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +5 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/io.d.ts +12 -0
  36. package/dist/io.d.ts.map +1 -0
  37. package/dist/io.js +20 -0
  38. package/dist/io.js.map +1 -0
  39. package/dist/learnabai.d.ts +60 -0
  40. package/dist/learnabai.d.ts.map +1 -0
  41. package/dist/learnabai.js +197 -0
  42. package/dist/learnabai.js.map +1 -0
  43. package/dist/new.d.ts +26 -0
  44. package/dist/new.d.ts.map +1 -0
  45. package/dist/new.js +87 -0
  46. package/dist/new.js.map +1 -0
  47. package/dist/repo.d.ts +99 -0
  48. package/dist/repo.d.ts.map +1 -0
  49. package/dist/repo.js +482 -0
  50. package/dist/repo.js.map +1 -0
  51. package/dist/run.d.ts +6 -0
  52. package/dist/run.d.ts.map +1 -0
  53. package/dist/run.js +47 -0
  54. package/dist/run.js.map +1 -0
  55. package/dist/subject.d.ts +15 -0
  56. package/dist/subject.d.ts.map +1 -0
  57. package/dist/subject.js +53 -0
  58. package/dist/subject.js.map +1 -0
  59. package/dist/template-ts.d.ts +21 -0
  60. package/dist/template-ts.d.ts.map +1 -0
  61. package/dist/template-ts.js +322 -0
  62. package/dist/template-ts.js.map +1 -0
  63. package/dist/version.d.ts +3 -0
  64. package/dist/version.d.ts.map +1 -0
  65. package/dist/version.js +3 -0
  66. package/dist/version.js.map +1 -0
  67. package/examples/hello/abai/identity.md +1 -0
  68. package/examples/hello/abaigram.yaml +24 -0
  69. package/examples/hello/hello.ts +18 -0
  70. package/package.json +40 -5
@@ -0,0 +1 @@
1
+ {"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../src/new.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,KAAK,SAAS,EAAc,MAAM,SAAS,CAAC;AAIrD,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAKD,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,CAkC5D;AAKD,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC;;;EAepD;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,EAAE,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAczG","sourcesContent":["// abaikit new: a fresh abaigram in a language, at a level, from the\n// template. The declaration is data: it becomes the project's `src/abai.ts`\n// and, through the stub's own `buildManifest`, its `abaigram.yaml`, so the\n// two agree before the program has ever run; `abaikit check` then proves\n// the level. Only TypeScript so far.\nimport { existsSync, mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { buildManifest, renderManifestYaml } from \"@abaigram/abai-stub\";\nimport type { ParsedArgs } from \"./bin.ts\";\nimport { type AbaikitIo, UsageError } from \"./io.ts\";\nimport { packageSpecs } from \"./learnabai.ts\";\nimport { declarationsFor, type Level, templateFiles } from \"./template-ts.ts\";\n\nexport interface NewOptions {\n\tname: string;\n\tlang: string;\n\tlevel?: number;\n\tdir?: string;\n\tabaiImport?: string;\n\tcwd: string;\n}\n\nexport interface NewResult {\n\tpath: string;\n\twritten: string[];\n\t/** Something the person must do before the project runs, when there is one. */\n\tnote?: string;\n}\n\n/** The stub's rule for a program's name, so the manifest validates. */\nconst NAME = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$/;\n\nexport function createProject(options: NewOptions): NewResult {\n\tif (options.lang !== \"ts\") throw new UsageError(`unknown language ${options.lang}; only ts is supported`);\n\tif (!NAME.test(options.name)) {\n\t\tthrow new UsageError(\n\t\t\t`${options.name} is not a name the manifest accepts (lower-case letters, digits, _ and . between words)`,\n\t\t);\n\t}\n\tconst level = options.level ?? 1;\n\tif (![1, 2, 3, 4].includes(level)) throw new UsageError(\"level must be 1 to 4\");\n\tconst path = resolve(options.cwd, options.dir ?? \".\", options.name);\n\tif (existsSync(path)) throw new UsageError(`${path} exists already; new creates a directory of its own`);\n\tconst specs = packageSpecs();\n\tconst input = {\n\t\tname: options.name,\n\t\tlevel: level as Level,\n\t\tabaiImport: options.abaiImport ?? \"@abaigram/abai-stub\",\n\t\tpackages: { ...specs.dependency, fromWorkspace: specs.fromWorkspace, built: specs.built },\n\t};\n\tconst files = templateFiles(input);\n\tfiles[\"abaigram.yaml\"] = renderManifestYaml(buildManifest(declarationsFor(input)));\n\tconst written: string[] = [];\n\ttry {\n\t\tfor (const [relative, content] of Object.entries(files)) {\n\t\t\tconst file = join(path, relative);\n\t\t\tmkdirSync(dirname(file), { recursive: true });\n\t\t\twriteFileSync(file, content);\n\t\t\twritten.push(relative);\n\t\t}\n\t} catch (error) {\n\t\t// nothing half made: the directory is new's own, so it goes with the failure\n\t\trmSync(path, { recursive: true, force: true });\n\t\tthrow error;\n\t}\n\treturn { path, written, note: specs.fromWorkspace && !specs.built ? UNBUILT_NOTE : undefined };\n}\n\nconst UNBUILT_NOTE =\n\t\"the dependencies are workspace checkouts whose dist/ is not built yet: run `npm run build` in the workspace before `npm install` here\";\n\n/** The `new` capability: the same creation, for the agent on any front end. */\nexport function newHandler(io: Pick<AbaikitIo, \"cwd\">) {\n\treturn (params: Record<string, unknown>): { path: string; written: number } => {\n\t\tif (typeof params.name !== \"string\" || params.name === \"\") throw new UsageError(\"new needs a name\");\n\t\tif (params.level !== undefined && (typeof params.level !== \"number\" || !Number.isInteger(params.level))) {\n\t\t\tthrow new UsageError(\"level must be 1 to 4\");\n\t\t}\n\t\tconst result = createProject({\n\t\t\tname: params.name,\n\t\t\tlang: typeof params.lang === \"string\" && params.lang !== \"\" ? params.lang : \"ts\",\n\t\t\tlevel: typeof params.level === \"number\" ? params.level : undefined,\n\t\t\tdir: typeof params.dir === \"string\" && params.dir !== \"\" ? params.dir : undefined,\n\t\t\tcwd: io.cwd,\n\t\t});\n\t\treturn { path: result.path, written: result.written.length };\n\t};\n}\n\nexport async function runNew(parsed: Extract<ParsedArgs, { verb: \"new\" }>, io: AbaikitIo): Promise<number> {\n\tconst result = createProject({\n\t\tname: parsed.name,\n\t\tlang: parsed.lang,\n\t\tlevel: parsed.level,\n\t\tdir: parsed.dir,\n\t\tabaiImport: parsed.abaiImport,\n\t\tcwd: io.cwd,\n\t});\n\tio.out(`${result.path}\\n`);\n\tfor (const file of result.written) io.out(` ${file}\\n`);\n\tif (result.note) io.out(`note: ${result.note}\\n`);\n\tio.out(`next: cd ${result.path} && npm install && node src/main.ts abai doctor --offline && npm run check\\n`);\n\treturn 0;\n}\n"]}
package/dist/new.js ADDED
@@ -0,0 +1,87 @@
1
+ // abaikit new: a fresh abaigram in a language, at a level, from the
2
+ // template. The declaration is data: it becomes the project's `src/abai.ts`
3
+ // and, through the stub's own `buildManifest`, its `abaigram.yaml`, so the
4
+ // two agree before the program has ever run; `abaikit check` then proves
5
+ // the level. Only TypeScript so far.
6
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { buildManifest, renderManifestYaml } from "@abaigram/abai-stub";
9
+ import { UsageError } from "./io.js";
10
+ import { packageSpecs } from "./learnabai.js";
11
+ import { declarationsFor, templateFiles } from "./template-ts.js";
12
+ /** The stub's rule for a program's name, so the manifest validates. */
13
+ const NAME = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/;
14
+ export function createProject(options) {
15
+ if (options.lang !== "ts")
16
+ throw new UsageError(`unknown language ${options.lang}; only ts is supported`);
17
+ if (!NAME.test(options.name)) {
18
+ throw new UsageError(`${options.name} is not a name the manifest accepts (lower-case letters, digits, _ and . between words)`);
19
+ }
20
+ const level = options.level ?? 1;
21
+ if (![1, 2, 3, 4].includes(level))
22
+ throw new UsageError("level must be 1 to 4");
23
+ const path = resolve(options.cwd, options.dir ?? ".", options.name);
24
+ if (existsSync(path))
25
+ throw new UsageError(`${path} exists already; new creates a directory of its own`);
26
+ const specs = packageSpecs();
27
+ const input = {
28
+ name: options.name,
29
+ level: level,
30
+ abaiImport: options.abaiImport ?? "@abaigram/abai-stub",
31
+ packages: { ...specs.dependency, fromWorkspace: specs.fromWorkspace, built: specs.built },
32
+ };
33
+ const files = templateFiles(input);
34
+ files["abaigram.yaml"] = renderManifestYaml(buildManifest(declarationsFor(input)));
35
+ const written = [];
36
+ try {
37
+ for (const [relative, content] of Object.entries(files)) {
38
+ const file = join(path, relative);
39
+ mkdirSync(dirname(file), { recursive: true });
40
+ writeFileSync(file, content);
41
+ written.push(relative);
42
+ }
43
+ }
44
+ catch (error) {
45
+ // nothing half made: the directory is new's own, so it goes with the failure
46
+ rmSync(path, { recursive: true, force: true });
47
+ throw error;
48
+ }
49
+ return { path, written, note: specs.fromWorkspace && !specs.built ? UNBUILT_NOTE : undefined };
50
+ }
51
+ const UNBUILT_NOTE = "the dependencies are workspace checkouts whose dist/ is not built yet: run `npm run build` in the workspace before `npm install` here";
52
+ /** The `new` capability: the same creation, for the agent on any front end. */
53
+ export function newHandler(io) {
54
+ return (params) => {
55
+ if (typeof params.name !== "string" || params.name === "")
56
+ throw new UsageError("new needs a name");
57
+ if (params.level !== undefined && (typeof params.level !== "number" || !Number.isInteger(params.level))) {
58
+ throw new UsageError("level must be 1 to 4");
59
+ }
60
+ const result = createProject({
61
+ name: params.name,
62
+ lang: typeof params.lang === "string" && params.lang !== "" ? params.lang : "ts",
63
+ level: typeof params.level === "number" ? params.level : undefined,
64
+ dir: typeof params.dir === "string" && params.dir !== "" ? params.dir : undefined,
65
+ cwd: io.cwd,
66
+ });
67
+ return { path: result.path, written: result.written.length };
68
+ };
69
+ }
70
+ export async function runNew(parsed, io) {
71
+ const result = createProject({
72
+ name: parsed.name,
73
+ lang: parsed.lang,
74
+ level: parsed.level,
75
+ dir: parsed.dir,
76
+ abaiImport: parsed.abaiImport,
77
+ cwd: io.cwd,
78
+ });
79
+ io.out(`${result.path}\n`);
80
+ for (const file of result.written)
81
+ io.out(` ${file}\n`);
82
+ if (result.note)
83
+ io.out(`note: ${result.note}\n`);
84
+ io.out(`next: cd ${result.path} && npm install && node src/main.ts abai doctor --offline && npm run check\n`);
85
+ return 0;
86
+ }
87
+ //# sourceMappingURL=new.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"new.js","sourceRoot":"","sources":["../src/new.ts"],"names":[],"mappings":"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","sourcesContent":["// abaikit new: a fresh abaigram in a language, at a level, from the\n// template. The declaration is data: it becomes the project's `src/abai.ts`\n// and, through the stub's own `buildManifest`, its `abaigram.yaml`, so the\n// two agree before the program has ever run; `abaikit check` then proves\n// the level. Only TypeScript so far.\nimport { existsSync, mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { buildManifest, renderManifestYaml } from \"@abaigram/abai-stub\";\nimport type { ParsedArgs } from \"./bin.ts\";\nimport { type AbaikitIo, UsageError } from \"./io.ts\";\nimport { packageSpecs } from \"./learnabai.ts\";\nimport { declarationsFor, type Level, templateFiles } from \"./template-ts.ts\";\n\nexport interface NewOptions {\n\tname: string;\n\tlang: string;\n\tlevel?: number;\n\tdir?: string;\n\tabaiImport?: string;\n\tcwd: string;\n}\n\nexport interface NewResult {\n\tpath: string;\n\twritten: string[];\n\t/** Something the person must do before the project runs, when there is one. */\n\tnote?: string;\n}\n\n/** The stub's rule for a program's name, so the manifest validates. */\nconst NAME = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$/;\n\nexport function createProject(options: NewOptions): NewResult {\n\tif (options.lang !== \"ts\") throw new UsageError(`unknown language ${options.lang}; only ts is supported`);\n\tif (!NAME.test(options.name)) {\n\t\tthrow new UsageError(\n\t\t\t`${options.name} is not a name the manifest accepts (lower-case letters, digits, _ and . between words)`,\n\t\t);\n\t}\n\tconst level = options.level ?? 1;\n\tif (![1, 2, 3, 4].includes(level)) throw new UsageError(\"level must be 1 to 4\");\n\tconst path = resolve(options.cwd, options.dir ?? \".\", options.name);\n\tif (existsSync(path)) throw new UsageError(`${path} exists already; new creates a directory of its own`);\n\tconst specs = packageSpecs();\n\tconst input = {\n\t\tname: options.name,\n\t\tlevel: level as Level,\n\t\tabaiImport: options.abaiImport ?? \"@abaigram/abai-stub\",\n\t\tpackages: { ...specs.dependency, fromWorkspace: specs.fromWorkspace, built: specs.built },\n\t};\n\tconst files = templateFiles(input);\n\tfiles[\"abaigram.yaml\"] = renderManifestYaml(buildManifest(declarationsFor(input)));\n\tconst written: string[] = [];\n\ttry {\n\t\tfor (const [relative, content] of Object.entries(files)) {\n\t\t\tconst file = join(path, relative);\n\t\t\tmkdirSync(dirname(file), { recursive: true });\n\t\t\twriteFileSync(file, content);\n\t\t\twritten.push(relative);\n\t\t}\n\t} catch (error) {\n\t\t// nothing half made: the directory is new's own, so it goes with the failure\n\t\trmSync(path, { recursive: true, force: true });\n\t\tthrow error;\n\t}\n\treturn { path, written, note: specs.fromWorkspace && !specs.built ? UNBUILT_NOTE : undefined };\n}\n\nconst UNBUILT_NOTE =\n\t\"the dependencies are workspace checkouts whose dist/ is not built yet: run `npm run build` in the workspace before `npm install` here\";\n\n/** The `new` capability: the same creation, for the agent on any front end. */\nexport function newHandler(io: Pick<AbaikitIo, \"cwd\">) {\n\treturn (params: Record<string, unknown>): { path: string; written: number } => {\n\t\tif (typeof params.name !== \"string\" || params.name === \"\") throw new UsageError(\"new needs a name\");\n\t\tif (params.level !== undefined && (typeof params.level !== \"number\" || !Number.isInteger(params.level))) {\n\t\t\tthrow new UsageError(\"level must be 1 to 4\");\n\t\t}\n\t\tconst result = createProject({\n\t\t\tname: params.name,\n\t\t\tlang: typeof params.lang === \"string\" && params.lang !== \"\" ? params.lang : \"ts\",\n\t\t\tlevel: typeof params.level === \"number\" ? params.level : undefined,\n\t\t\tdir: typeof params.dir === \"string\" && params.dir !== \"\" ? params.dir : undefined,\n\t\t\tcwd: io.cwd,\n\t\t});\n\t\treturn { path: result.path, written: result.written.length };\n\t};\n}\n\nexport async function runNew(parsed: Extract<ParsedArgs, { verb: \"new\" }>, io: AbaikitIo): Promise<number> {\n\tconst result = createProject({\n\t\tname: parsed.name,\n\t\tlang: parsed.lang,\n\t\tlevel: parsed.level,\n\t\tdir: parsed.dir,\n\t\tabaiImport: parsed.abaiImport,\n\t\tcwd: io.cwd,\n\t});\n\tio.out(`${result.path}\\n`);\n\tfor (const file of result.written) io.out(` ${file}\\n`);\n\tif (result.note) io.out(`note: ${result.note}\\n`);\n\tio.out(`next: cd ${result.path} && npm install && node src/main.ts abai doctor --offline && npm run check\\n`);\n\treturn 0;\n}\n"]}
package/dist/repo.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type { Abaigram } from "@abaigram/abai-stub";
2
+ export declare const LIST_CAP = 400;
3
+ export declare const LIST_CAP_BYTES = 12000;
4
+ export declare const READ_CAP_LINES = 400;
5
+ export declare const READ_CAP_BYTES = 12000;
6
+ export declare const INSTALL_TIMEOUT_MS: number;
7
+ export declare const MANIFEST_TIMEOUT_MS = 60000;
8
+ /** Trees skipped everywhere. */
9
+ export declare const SKIPPED_EVERYWHERE: Set<string>;
10
+ /** Trees skipped at the root: build output, vendored and generated sites (a `src/build/` is the program's own). */
11
+ export declare const SKIPPED_AT_ROOT: Set<string>;
12
+ export interface Change {
13
+ how: "write" | "insert" | "add_dependency" | "manifest" | "install";
14
+ path: string;
15
+ why: string;
16
+ detail: string;
17
+ /** Added to the repository (a new file, inserted lines) or rewritten in place by a structured tool. */
18
+ kind: "added" | "rewritten";
19
+ at: string;
20
+ }
21
+ export interface RepoContext {
22
+ /** Where relative `repo` arguments resolve. */
23
+ cwd: string;
24
+ env: NodeJS.ProcessEnv;
25
+ /** The abaigram whose `repo.changed` event every change goes to; undefined under a plain unit test. */
26
+ app?: Abaigram;
27
+ /** Nothing is written; the write capabilities say what they would do. */
28
+ dryRun?: boolean;
29
+ changes: Change[];
30
+ /** Told of each change as it happens, for a person watching. */
31
+ onChange?: (change: Change) => void;
32
+ /** Files this run created, which it may write again: nothing the repository had. */
33
+ created?: Set<string>;
34
+ }
35
+ interface IgnoreRule {
36
+ regex: RegExp;
37
+ dirOnly: boolean;
38
+ anchored: boolean;
39
+ }
40
+ export declare function ignoreRules(root: string): IgnoreRule[];
41
+ export interface Listing {
42
+ /** Entries in `listing`. */
43
+ count: number;
44
+ /** Files that matched, listed or not. */
45
+ total: number;
46
+ listing: string;
47
+ truncated: number;
48
+ }
49
+ /**
50
+ * `repo.list`: files under the root, skipping what no retrofit reads,
51
+ * bounded by entries and bytes, the bound said first. A glob narrows:
52
+ * `*.json` by name anywhere, `src/**` or `scripts/*.mjs` by path.
53
+ */
54
+ export declare function listRepo(root: string, glob?: string): Listing;
55
+ /** The shape of a repository for a brief: the files at the root, and each directory with its file count. */
56
+ export declare function summarizeRepo(root: string): {
57
+ total: number;
58
+ summary: string;
59
+ };
60
+ /** `repo.read`: a window of a file from line `start` (1-based), at most `READ_CAP_LINES` lines and `READ_CAP_BYTES` bytes. */
61
+ export declare function readRepo(root: string, path: string, start?: number, lines?: number): {
62
+ text: string;
63
+ total_lines: number;
64
+ from: number;
65
+ to: number;
66
+ };
67
+ /**
68
+ * `repo.write`: a new file, or one this run made and may now correct; a
69
+ * file the repository had is refused, since abaify adds and never replaces.
70
+ */
71
+ export declare function writeRepo(ctx: RepoContext, root: string, path: string, content: string, why: string): {
72
+ written: boolean;
73
+ path: string;
74
+ };
75
+ /** `repo.insert`: `text` before or after the one line equal to `anchor` (trimmed); the original bytes stay, in order. */
76
+ export declare function insertRepo(ctx: RepoContext, root: string, path: string, anchor: string, where: string, text: string, why: string): {
77
+ line: number;
78
+ path: string;
79
+ };
80
+ /** `repo.add_dependency`: one entry added to `dependencies`, in sorted place; an existing one is left as it is and said. */
81
+ export declare function addDependency(ctx: RepoContext, root: string, name: string, spec: string | undefined, why: string): {
82
+ spec: string;
83
+ added: boolean;
84
+ };
85
+ /** `repo.manifest`: the program's own `<entry> manifest`, written to abaigram.yaml, the one file never written by hand. */
86
+ export declare function regenerateManifest(ctx: RepoContext, root: string, entry: string, why: string): Promise<{
87
+ written: boolean;
88
+ lines: number;
89
+ text: string;
90
+ }>;
91
+ /** `repo.install`: `npm install` in the root, bounded; the one step that reaches the network and runs the repository's own install scripts. */
92
+ export declare function installRepo(ctx: RepoContext, root: string, why: string): Promise<{
93
+ ok: boolean;
94
+ output: string;
95
+ }>;
96
+ /** The capabilities, bound to a context, in the shape abaikit's manifest declares them. */
97
+ export declare function repoHandlers(ctx: RepoContext): Record<string, (params: Record<string, unknown>) => unknown>;
98
+ export {};
99
+ //# sourceMappingURL=repo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../src/repo.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAMpD,eAAO,MAAM,QAAQ,MAAM,CAAC;AAC5B,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAC9C,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C,gCAAgC;AAChC,eAAO,MAAM,kBAAkB,aAAoC,CAAC;AACpE,mHAAmH;AACnH,eAAO,MAAM,eAAe,aAW1B,CAAC;AAEH,MAAM,WAAW,MAAM;IACtB,GAAG,EAAE,OAAO,GAAG,QAAQ,GAAG,gBAAgB,GAAG,UAAU,GAAG,SAAS,CAAC;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,uGAAuG;IACvG,IAAI,EAAE,OAAO,GAAG,WAAW,CAAC;IAC5B,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,WAAW;IAC3B,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,uGAAuG;IACvG,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf,yEAAyE;IACzE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,oFAAoF;IACpF,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACtB;AAuDD,UAAU,UAAU;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CAClB;AAkBD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CActD;AAoBD,MAAM,WAAW,OAAO;IACvB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CAClB;AA2BD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAmB7D;AAED,4GAA4G;AAC5G,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA4B9E;AAED,8HAA8H;AAC9H,wBAAgB,QAAQ,CACvB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,SAAI,EACT,KAAK,SAAiB,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAcjE;AAaD;;;GAGG;AACH,wBAAgB,SAAS,CACxB,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACT;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA+BpC;AAED,yHAAyH;AACzH,wBAAgB,UAAU,CACzB,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACT;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA2ChC;AAQD,4HAA4H;AAC5H,wBAAgB,aAAa,CAC5B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,GAAG,EAAE,MAAM,GACT;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAkDlC;AAED,2HAA2H;AAC3H,wBAAsB,kBAAkB,CACvC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACT,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAoC5D;AAED,+IAA+I;AAC/I,wBAAsB,WAAW,CAChC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACT,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAmC1C;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAwB3G","sourcesContent":["// The repository capabilities an abaify agent works through, and nothing\n// else: it reads what it likes within bounds and changes a repository only\n// by adding. A new file; text inserted before or after a unique anchor\n// line, with the original bytes kept in order; a dependency added to\n// package.json, which is re-serialized by a structured edit; the manifest\n// regenerated by the program's own entry, replacing abaigram.yaml when\n// there was one; the dependencies installed, which rewrites the lock file\n// and node_modules. No source line the repository had is ever removed or\n// reordered. Every change is recorded for the report and emitted as\n// abaikit's own `repo.changed` event.\nimport { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\nimport type { Abaigram } from \"@abaigram/abai-stub\";\nimport { runCommand, subjectEnv } from \"./command.ts\";\nimport { UsageError } from \"./io.ts\";\nimport { packageSpecs } from \"./learnabai.ts\";\nimport { shellQuote } from \"./subject.ts\";\n\nexport const LIST_CAP = 400;\nexport const LIST_CAP_BYTES = 12_000;\nexport const READ_CAP_LINES = 400;\nexport const READ_CAP_BYTES = 12_000;\nexport const INSTALL_TIMEOUT_MS = 10 * 60_000;\nexport const MANIFEST_TIMEOUT_MS = 60_000;\n\n/** Trees skipped everywhere. */\nexport const SKIPPED_EVERYWHERE = new Set([\".git\", \"node_modules\"]);\n/** Trees skipped at the root: build output, vendored and generated sites (a `src/build/` is the program's own). */\nexport const SKIPPED_AT_ROOT = new Set([\n\t\"dist\",\n\t\"build\",\n\t\"third_party\",\n\t\"vendor\",\n\t\"website\",\n\t\"docs\",\n\t\"coverage\",\n\t\".turbo\",\n\t\".next\",\n\t\"out\",\n]);\n\nexport interface Change {\n\thow: \"write\" | \"insert\" | \"add_dependency\" | \"manifest\" | \"install\";\n\tpath: string;\n\twhy: string;\n\tdetail: string;\n\t/** Added to the repository (a new file, inserted lines) or rewritten in place by a structured tool. */\n\tkind: \"added\" | \"rewritten\";\n\tat: string;\n}\n\nexport interface RepoContext {\n\t/** Where relative `repo` arguments resolve. */\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n\t/** The abaigram whose `repo.changed` event every change goes to; undefined under a plain unit test. */\n\tapp?: Abaigram;\n\t/** Nothing is written; the write capabilities say what they would do. */\n\tdryRun?: boolean;\n\tchanges: Change[];\n\t/** Told of each change as it happens, for a person watching. */\n\tonChange?: (change: Change) => void;\n\t/** Files this run created, which it may write again: nothing the repository had. */\n\tcreated?: Set<string>;\n}\n\nfunction str(params: Record<string, unknown>, name: string, required = true): string {\n\tconst value = params[name];\n\tif (typeof value === \"string\" && value !== \"\") return value;\n\tif (required) throw new UsageError(`${name} is required`);\n\treturn \"\";\n}\n\nfunction text(params: Record<string, unknown>, name: string): string {\n\tconst value = params[name];\n\tif (typeof value !== \"string\") throw new UsageError(`${name} is required (a string, empty allowed)`);\n\treturn value;\n}\n\nfunction num(params: Record<string, unknown>, name: string): number | undefined {\n\tconst value = params[name];\n\tif (value === undefined || value === null) return undefined;\n\tif (typeof value !== \"number\" || !Number.isInteger(value) || value < 0)\n\t\tthrow new UsageError(`${name} must be a whole number`);\n\treturn value;\n}\n\n/** The repository root, an existing directory; and a path inside it, never outside. */\nfunction repoRoot(ctx: RepoContext, params: Record<string, unknown>): string {\n\tconst root = resolve(ctx.cwd, str(params, \"repo\"));\n\tif (!existsSync(root) || !statSync(root).isDirectory()) throw new UsageError(`${root} is not a directory`);\n\treturn root;\n}\n\n/** The real path of the nearest existing ancestor plus the rest, so a symlink pointing out is seen. */\nfunction realish(path: string): string {\n\tlet existing = path;\n\tconst rest: string[] = [];\n\twhile (!existsSync(existing)) {\n\t\tconst parent = dirname(existing);\n\t\tif (parent === existing) break;\n\t\trest.unshift(existing.slice(parent.length + 1));\n\t\texisting = parent;\n\t}\n\treturn join(realpathSync(existing), ...rest);\n}\n\nfunction inside(root: string, path: string): string {\n\tconst realRoot = realpathSync(root);\n\tconst full = realish(resolve(root, path));\n\tif (full !== realRoot && !full.startsWith(realRoot + sep)) throw new UsageError(`${path} is outside the repository`);\n\treturn resolve(root, path);\n}\n\n// ---------------------------------------------------------------------------\n// .gitignore, enough of it: names, anchored paths, directory rules, `*` and `?`;\n// no negations, no character classes (they are escaped, so never match)\n// ---------------------------------------------------------------------------\n\ninterface IgnoreRule {\n\tregex: RegExp;\n\tdirOnly: boolean;\n\tanchored: boolean;\n}\n\nfunction globToRegex(glob: string): string {\n\tlet out = \"\";\n\tfor (let i = 0; i < glob.length; i++) {\n\t\tconst c = glob[i];\n\t\tif (c === \"*\") {\n\t\t\tif (glob[i + 1] === \"*\") {\n\t\t\t\tout += \".*\";\n\t\t\t\ti += 1;\n\t\t\t\tif (glob[i + 1] === \"/\") i += 1;\n\t\t\t} else out += \"[^/]*\";\n\t\t} else if (c === \"?\") out += \"[^/]\";\n\t\telse out += c.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t}\n\treturn out;\n}\n\nexport function ignoreRules(root: string): IgnoreRule[] {\n\tconst file = join(root, \".gitignore\");\n\tif (!existsSync(file)) return [];\n\tconst rules: IgnoreRule[] = [];\n\tfor (const raw of readFileSync(file, \"utf8\").split(\"\\n\")) {\n\t\tconst line = raw.trim();\n\t\tif (line === \"\" || line.startsWith(\"#\") || line.startsWith(\"!\")) continue;\n\t\tconst dirOnly = line.endsWith(\"/\");\n\t\tlet pattern = dirOnly ? line.slice(0, -1) : line;\n\t\tconst anchored = pattern.startsWith(\"/\") || pattern.includes(\"/\");\n\t\tif (pattern.startsWith(\"/\")) pattern = pattern.slice(1);\n\t\trules.push({ regex: new RegExp(`^${globToRegex(pattern)}$`), dirOnly, anchored });\n\t}\n\treturn rules;\n}\n\nfunction ignored(rules: IgnoreRule[], relativePath: string, isDir: boolean): boolean {\n\tconst name = relativePath.split(\"/\").at(-1) ?? \"\";\n\treturn rules.some((rule) => {\n\t\tif (rule.dirOnly && !isDir) return false;\n\t\treturn rule.anchored ? rule.regex.test(relativePath) : rule.regex.test(name);\n\t});\n}\n\nfunction skipped(rules: IgnoreRule[], name: string, rel: string, depth: number): boolean {\n\tif (SKIPPED_EVERYWHERE.has(name)) return true;\n\tif (depth === 0 && SKIPPED_AT_ROOT.has(name)) return true;\n\treturn ignored(rules, rel, true);\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\nexport interface Listing {\n\t/** Entries in `listing`. */\n\tcount: number;\n\t/** Files that matched, listed or not. */\n\ttotal: number;\n\tlisting: string;\n\ttruncated: number;\n}\n\n/** Every file under `dir` (relative paths), the skips applied, in sorted order. */\nfunction walk(root: string, rules: IgnoreRule[], dir: string, depth: number, visit: (rel: string) => void): void {\n\tlet entries: string[];\n\ttry {\n\t\tentries = readdirSync(dir).sort();\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const name of entries) {\n\t\tconst full = join(dir, name);\n\t\tconst rel = relative(root, full).split(sep).join(\"/\");\n\t\tlet isDir: boolean;\n\t\ttry {\n\t\t\tisDir = statSync(full).isDirectory();\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (isDir) {\n\t\t\tif (!skipped(rules, name, rel, depth)) walk(root, rules, full, depth + 1, visit);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!ignored(rules, rel, false)) visit(rel);\n\t}\n}\n\n/**\n * `repo.list`: files under the root, skipping what no retrofit reads,\n * bounded by entries and bytes, the bound said first. A glob narrows:\n * `*.json` by name anywhere, `src/**` or `scripts/*.mjs` by path.\n */\nexport function listRepo(root: string, glob?: string): Listing {\n\tconst rules = ignoreRules(root);\n\tconst filter = glob ? new RegExp(`^${globToRegex(glob)}$`) : undefined;\n\tconst found: string[] = [];\n\tlet total = 0;\n\tlet bytes = 0;\n\twalk(root, rules, root, 0, (rel) => {\n\t\tif (filter && !filter.test(rel) && !filter.test(rel.split(\"/\").at(-1) ?? \"\")) return;\n\t\ttotal += 1;\n\t\tif (found.length >= LIST_CAP || bytes + rel.length + 1 > LIST_CAP_BYTES) return;\n\t\tfound.push(rel);\n\t\tbytes += rel.length + 1;\n\t});\n\tconst truncated = total - found.length;\n\tconst head =\n\t\ttruncated > 0\n\t\t\t? `[${found.length} of ${total} files listed; ${truncated} more: narrow with glob, such as src/** or *.ts]\\n`\n\t\t\t: \"\";\n\treturn { count: found.length, total, listing: head + found.join(\"\\n\"), truncated };\n}\n\n/** The shape of a repository for a brief: the files at the root, and each directory with its file count. */\nexport function summarizeRepo(root: string): { total: number; summary: string } {\n\tconst rules = ignoreRules(root);\n\tconst files: string[] = [];\n\tconst dirs: Array<{ name: string; files: number }> = [];\n\tlet total = 0;\n\tfor (const name of readdirSync(root).sort()) {\n\t\tconst full = join(root, name);\n\t\tlet isDir: boolean;\n\t\ttry {\n\t\t\tisDir = statSync(full).isDirectory();\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (isDir) {\n\t\t\tif (skipped(rules, name, name, 0)) continue;\n\t\t\tlet n = 0;\n\t\t\twalk(root, rules, full, 1, () => {\n\t\t\t\tn += 1;\n\t\t\t});\n\t\t\tdirs.push({ name, files: n });\n\t\t\ttotal += n;\n\t\t} else if (!ignored(rules, name, false)) {\n\t\t\tfiles.push(name);\n\t\t\ttotal += 1;\n\t\t}\n\t}\n\tconst lines = [...files, ...dirs.map((d) => `${d.name}/ (${d.files} file${d.files === 1 ? \"\" : \"s\"})`)];\n\treturn { total, summary: lines.join(\"\\n\") };\n}\n\n/** `repo.read`: a window of a file from line `start` (1-based), at most `READ_CAP_LINES` lines and `READ_CAP_BYTES` bytes. */\nexport function readRepo(\n\troot: string,\n\tpath: string,\n\tstart = 1,\n\tlines = READ_CAP_LINES,\n): { text: string; total_lines: number; from: number; to: number } {\n\tconst full = inside(root, path);\n\tif (!existsSync(full) || statSync(full).isDirectory()) throw new UsageError(`${path}: no such file`);\n\tconst all = readFileSync(full, \"utf8\").split(\"\\n\");\n\tconst count = Math.min(Math.max(1, lines), READ_CAP_LINES);\n\tconst from = Math.max(1, start);\n\tconst picked: string[] = [];\n\tlet bytes = 0;\n\tfor (const line of all.slice(from - 1, from - 1 + count)) {\n\t\tif (picked.length > 0 && bytes + line.length + 1 > READ_CAP_BYTES) break;\n\t\tpicked.push(line);\n\t\tbytes += line.length + 1;\n\t}\n\treturn { text: picked.join(\"\\n\"), total_lines: all.length, from, to: from - 1 + picked.length };\n}\n\n// ---------------------------------------------------------------------------\n// Adding\n// ---------------------------------------------------------------------------\n\nfunction record(ctx: RepoContext, change: Omit<Change, \"at\">): void {\n\tconst full = { ...change, at: new Date().toISOString() };\n\tctx.changes.push(full);\n\tctx.onChange?.(full);\n\tctx.app?.event(\"repo.changed\").emit({ path: change.path, how: change.how });\n}\n\n/**\n * `repo.write`: a new file, or one this run made and may now correct; a\n * file the repository had is refused, since abaify adds and never replaces.\n */\nexport function writeRepo(\n\tctx: RepoContext,\n\troot: string,\n\tpath: string,\n\tcontent: string,\n\twhy: string,\n): { written: boolean; path: string } {\n\tconst full = inside(root, path);\n\tconst own = ctx.created?.has(full) === true;\n\tif (existsSync(full) && !own) {\n\t\tthrow new UsageError(\n\t\t\t`${path} exists already; repo.write makes new files only (repo.insert adds to an existing one)`,\n\t\t);\n\t}\n\tconst lines = content.split(\"\\n\").length;\n\tif (ctx.dryRun) {\n\t\trecord(ctx, {\n\t\t\thow: \"write\",\n\t\t\tpath,\n\t\t\twhy,\n\t\t\tkind: \"added\",\n\t\t\tdetail: `dry run: would write ${content.length} characters`,\n\t\t});\n\t\tctx.created?.add(full);\n\t\treturn { written: false, path };\n\t}\n\tmkdirSync(dirname(full), { recursive: true });\n\twriteFileSync(full, content);\n\tctx.created?.add(full);\n\trecord(ctx, {\n\t\thow: \"write\",\n\t\tpath,\n\t\twhy,\n\t\tkind: \"added\",\n\t\tdetail: own ? `${lines} lines (this run's own file, written again)` : `${lines} lines`,\n\t});\n\treturn { written: true, path };\n}\n\n/** `repo.insert`: `text` before or after the one line equal to `anchor` (trimmed); the original bytes stay, in order. */\nexport function insertRepo(\n\tctx: RepoContext,\n\troot: string,\n\tpath: string,\n\tanchor: string,\n\twhere: string,\n\ttext: string,\n\twhy: string,\n): { line: number; path: string } {\n\tif (where !== \"before\" && where !== \"after\") throw new UsageError(`where must be before or after, not ${where}`);\n\tconst full = inside(root, path);\n\tif (!existsSync(full) || statSync(full).isDirectory())\n\t\tthrow new UsageError(`${path}: no such file (repo.write makes a new one)`);\n\tconst original = readFileSync(full, \"utf8\");\n\tconst lines = original.split(\"\\n\");\n\tconst wanted = anchor.trim();\n\tif (wanted === \"\") throw new UsageError(\"anchor must be a line of the file\");\n\tconst hits = lines.map((l, i) => (l.trim() === wanted ? i : -1)).filter((i) => i >= 0);\n\tif (hits.length === 0) throw new UsageError(`${path}: no line reads ${JSON.stringify(wanted)}`);\n\tif (hits.length > 1)\n\t\tthrow new UsageError(\n\t\t\t`${path}: ${hits.length} lines read ${JSON.stringify(wanted)} (lines ${hits.map((i) => i + 1).join(\", \")}); the anchor must be unique`,\n\t\t);\n\tconst at = hits[0] + (where === \"after\" ? 1 : 0);\n\tconst inserted = text.replace(/\\n$/, \"\").split(\"\\n\");\n\tif (ctx.dryRun) {\n\t\trecord(ctx, {\n\t\t\thow: \"insert\",\n\t\t\tpath,\n\t\t\twhy,\n\t\t\tkind: \"added\",\n\t\t\tdetail: `dry run: would insert ${inserted.length} lines ${where} line ${hits[0] + 1}`,\n\t\t});\n\t\treturn { line: at + 1, path };\n\t}\n\tconst before = lines.slice(0, at).join(\"\\n\");\n\tconst after = lines.slice(at).join(\"\\n\");\n\tconst updated = [...lines.slice(0, at), ...inserted, ...lines.slice(at)].join(\"\\n\");\n\t// the additive guarantee, checked, not assumed: what was there is still there, in order, around the insert\n\tif (!updated.startsWith(before) || !updated.endsWith(after)) {\n\t\tthrow new Error(`${path}: the insert would not have been additive; nothing was written`);\n\t}\n\twriteFileSync(full, updated);\n\trecord(ctx, {\n\t\thow: \"insert\",\n\t\tpath,\n\t\twhy,\n\t\tkind: \"added\",\n\t\tdetail: `${inserted.length} lines ${where} line ${hits[0] + 1}`,\n\t});\n\treturn { line: at + 1, path };\n}\n\n/** The indentation a JSON file uses, so an edit keeps its shape. */\nfunction indentOf(text: string): string {\n\tconst match = /^(\\t+| +)\"/m.exec(text);\n\treturn match ? match[1] : \" \";\n}\n\n/** `repo.add_dependency`: one entry added to `dependencies`, in sorted place; an existing one is left as it is and said. */\nexport function addDependency(\n\tctx: RepoContext,\n\troot: string,\n\tname: string,\n\tspec: string | undefined,\n\twhy: string,\n): { spec: string; added: boolean } {\n\tconst file = inside(root, \"package.json\");\n\tif (!existsSync(file)) throw new UsageError(\"package.json: no such file; repo.add_dependency needs one\");\n\tconst original = readFileSync(file, \"utf8\");\n\tlet pkg: Record<string, unknown>;\n\ttry {\n\t\tpkg = JSON.parse(original) as Record<string, unknown>;\n\t} catch (error) {\n\t\tthrow new UsageError(`package.json does not parse: ${error instanceof Error ? error.message : String(error)}`);\n\t}\n\tconst known = packageSpecs().dependency;\n\tconst chosen =\n\t\tspec ??\n\t\t(name === \"@abaigram/abai-stub\"\n\t\t\t? known.abai\n\t\t\t: name === \"@abaigram/abai-embedded-harness\"\n\t\t\t\t? known.harness\n\t\t\t\t: name === \"@abaigram/abaikit\"\n\t\t\t\t\t? known.abaikit\n\t\t\t\t\t: undefined);\n\tif (chosen === undefined)\n\t\tthrow new UsageError(\n\t\t\t`${name} needs a spec; only @abaigram/abai-stub, @abaigram/abai-embedded-harness and @abaigram/abaikit have a default here`,\n\t\t);\n\tconst deps = (pkg.dependencies ?? {}) as Record<string, string>;\n\tif (deps[name] !== undefined) return { spec: deps[name], added: false };\n\tif (ctx.dryRun) {\n\t\trecord(ctx, {\n\t\t\thow: \"add_dependency\",\n\t\t\tpath: \"package.json\",\n\t\t\twhy,\n\t\t\tkind: \"rewritten\",\n\t\t\tdetail: `dry run: would add ${name}: ${chosen}`,\n\t\t});\n\t\treturn { spec: chosen, added: false };\n\t}\n\tconst sorted: Record<string, string> = {};\n\tfor (const key of [...Object.keys(deps), name].sort()) sorted[key] = key === name ? chosen : deps[key];\n\tpkg.dependencies = sorted;\n\tconst eol = original.includes(\"\\r\\n\") ? \"\\r\\n\" : \"\\n\";\n\tconst serialized = JSON.stringify(pkg, null, indentOf(original)).split(\"\\n\").join(eol);\n\twriteFileSync(file, `${serialized}${/\\r?\\n$/.test(original) ? eol : \"\"}`);\n\trecord(ctx, {\n\t\thow: \"add_dependency\",\n\t\tpath: \"package.json\",\n\t\twhy,\n\t\tkind: \"rewritten\",\n\t\tdetail: `${name}: ${chosen} (the file re-serialized, keys and indentation kept)`,\n\t});\n\treturn { spec: chosen, added: true };\n}\n\n/** `repo.manifest`: the program's own `<entry> manifest`, written to abaigram.yaml, the one file never written by hand. */\nexport async function regenerateManifest(\n\tctx: RepoContext,\n\troot: string,\n\tentry: string,\n\twhy: string,\n): Promise<{ written: boolean; lines: number; text: string }> {\n\tconst run = await runCommand(`${entry} manifest`, {\n\t\tcwd: root,\n\t\tenv: subjectEnv(ctx.env),\n\t\ttimeoutMs: MANIFEST_TIMEOUT_MS,\n\t});\n\tif (run.code !== 0 || run.timedOut) {\n\t\tconst last = run.stderr.trim().split(\"\\n\").at(-1) ?? \"\";\n\t\tthrow new UsageError(\n\t\t\t`${entry} manifest ${run.timedOut ? \"did not finish\" : `exited ${run.code ?? run.signal}`}: ${last || \"no output\"}`,\n\t\t);\n\t}\n\tif (!run.stdout.includes(\"abaigrammar:\"))\n\t\tthrow new UsageError(`${entry} manifest printed no manifest: ${run.stdout.slice(0, 200)}`);\n\tconst target = join(root, \"abaigram.yaml\");\n\tconst previous = existsSync(target) ? readFileSync(target, \"utf8\").split(\"\\n\").length : undefined;\n\tconst lines = run.stdout.split(\"\\n\").length;\n\tif (ctx.dryRun) {\n\t\trecord(ctx, {\n\t\t\thow: \"manifest\",\n\t\t\tpath: \"abaigram.yaml\",\n\t\t\twhy,\n\t\t\tkind: previous === undefined ? \"added\" : \"rewritten\",\n\t\t\tdetail: \"dry run (the entry was run, read-only): would write what it prints\",\n\t\t});\n\t\treturn { written: false, lines, text: run.stdout };\n\t}\n\twriteFileSync(target, run.stdout);\n\trecord(ctx, {\n\t\thow: \"manifest\",\n\t\tpath: \"abaigram.yaml\",\n\t\twhy,\n\t\tkind: previous === undefined ? \"added\" : \"rewritten\",\n\t\tdetail: `${lines} lines, from ${entry} manifest${previous === undefined ? \"\" : ` (replaced a previous ${previous}-line file)`}`,\n\t});\n\treturn { written: true, lines, text: run.stdout };\n}\n\n/** `repo.install`: `npm install` in the root, bounded; the one step that reaches the network and runs the repository's own install scripts. */\nexport async function installRepo(\n\tctx: RepoContext,\n\troot: string,\n\twhy: string,\n): Promise<{ ok: boolean; output: string }> {\n\tif (ctx.dryRun) {\n\t\trecord(ctx, {\n\t\t\thow: \"install\",\n\t\t\tpath: \"node_modules\",\n\t\t\twhy,\n\t\t\tkind: \"rewritten\",\n\t\t\tdetail: \"dry run: would run npm install\",\n\t\t});\n\t\treturn { ok: true, output: \"dry run\" };\n\t}\n\tconst run = await runCommand(`npm install ${shellQuote(\"--no-audit\")} ${shellQuote(\"--no-fund\")}`, {\n\t\tcwd: root,\n\t\tenv: subjectEnv(ctx.env),\n\t\ttimeoutMs: INSTALL_TIMEOUT_MS,\n\t});\n\tconst tail = `${run.stdout}\\n${run.stderr}`.trim().split(\"\\n\").slice(-12).join(\"\\n\");\n\trecord(ctx, {\n\t\thow: \"install\",\n\t\tpath: \"node_modules\",\n\t\twhy,\n\t\tkind: \"rewritten\",\n\t\tdetail: run.code === 0 ? \"npm install ok\" : `npm install failed (${run.code ?? run.signal})`,\n\t});\n\tif (existsSync(join(root, \"package-lock.json\"))) {\n\t\trecord(ctx, {\n\t\t\thow: \"install\",\n\t\t\tpath: \"package-lock.json\",\n\t\t\twhy,\n\t\t\tkind: \"rewritten\",\n\t\t\tdetail: \"written by npm install\",\n\t\t});\n\t}\n\tif (run.code !== 0) throw new UsageError(`npm install failed: ${tail}`);\n\treturn { ok: true, output: tail };\n}\n\n/** The capabilities, bound to a context, in the shape abaikit's manifest declares them. */\nexport function repoHandlers(ctx: RepoContext): Record<string, (params: Record<string, unknown>) => unknown> {\n\treturn {\n\t\t\"repo.list\": (p) => {\n\t\t\tconst { count, total, listing } = listRepo(repoRoot(ctx, p), str(p, \"glob\", false) || undefined);\n\t\t\treturn { count, total, listing };\n\t\t},\n\t\t\"repo.read\": (p) =>\n\t\t\treadRepo(repoRoot(ctx, p), str(p, \"path\"), num(p, \"start\") ?? 1, num(p, \"lines\") ?? READ_CAP_LINES),\n\t\t\"repo.write\": (p) => writeRepo(ctx, repoRoot(ctx, p), str(p, \"path\"), text(p, \"content\"), str(p, \"why\")),\n\t\t\"repo.insert\": (p) =>\n\t\t\tinsertRepo(\n\t\t\t\tctx,\n\t\t\t\trepoRoot(ctx, p),\n\t\t\t\tstr(p, \"path\"),\n\t\t\t\tstr(p, \"anchor\"),\n\t\t\t\tstr(p, \"where\"),\n\t\t\t\ttext(p, \"text\"),\n\t\t\t\tstr(p, \"why\"),\n\t\t\t),\n\t\t\"repo.add_dependency\": (p) =>\n\t\t\taddDependency(ctx, repoRoot(ctx, p), str(p, \"name\"), str(p, \"spec\", false) || undefined, str(p, \"why\")),\n\t\t\"repo.manifest\": (p) => regenerateManifest(ctx, repoRoot(ctx, p), str(p, \"entry\"), str(p, \"why\")),\n\t\t\"repo.install\": (p) => installRepo(ctx, repoRoot(ctx, p), str(p, \"why\")),\n\t};\n}\n"]}