looply-cli 0.3.0 → 0.5.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 (88) hide show
  1. package/dist/.src-hash +1 -1
  2. package/dist/bin.d.ts.map +1 -1
  3. package/dist/bin.js +91 -85
  4. package/dist/bin.js.map +1 -1
  5. package/dist/connect-node.d.ts +8 -0
  6. package/dist/connect-node.d.ts.map +1 -0
  7. package/dist/connect-node.js +110 -0
  8. package/dist/connect-node.js.map +1 -0
  9. package/dist/connect.d.ts +135 -0
  10. package/dist/connect.d.ts.map +1 -0
  11. package/dist/connect.js +329 -0
  12. package/dist/connect.js.map +1 -0
  13. package/dist/daemon-cmd.d.ts +11 -2
  14. package/dist/daemon-cmd.d.ts.map +1 -1
  15. package/dist/daemon-cmd.js +20 -6
  16. package/dist/daemon-cmd.js.map +1 -1
  17. package/dist/goal-compile.d.ts.map +1 -1
  18. package/dist/goal-compile.js +4 -1
  19. package/dist/goal-compile.js.map +1 -1
  20. package/dist/mcp-main.js +10 -0
  21. package/dist/mcp-main.js.map +1 -1
  22. package/dist/mcp-server.d.ts +14 -0
  23. package/dist/mcp-server.d.ts.map +1 -1
  24. package/dist/mcp-server.js +35 -5
  25. package/dist/mcp-server.js.map +1 -1
  26. package/dist/model-transport.d.ts +7 -0
  27. package/dist/model-transport.d.ts.map +1 -1
  28. package/dist/model-transport.js +4 -1
  29. package/dist/model-transport.js.map +1 -1
  30. package/dist/packet-for-goal.d.ts +4 -2
  31. package/dist/packet-for-goal.d.ts.map +1 -1
  32. package/dist/packet-for-goal.js +4 -2
  33. package/dist/packet-for-goal.js.map +1 -1
  34. package/dist/repo-init.d.ts +36 -0
  35. package/dist/repo-init.d.ts.map +1 -0
  36. package/dist/repo-init.js +61 -0
  37. package/dist/repo-init.js.map +1 -0
  38. package/dist/repo-origin.d.ts +15 -0
  39. package/dist/repo-origin.d.ts.map +1 -0
  40. package/dist/repo-origin.js +28 -0
  41. package/dist/repo-origin.js.map +1 -0
  42. package/dist/repo-requests.d.ts +79 -0
  43. package/dist/repo-requests.d.ts.map +1 -0
  44. package/dist/repo-requests.js +180 -0
  45. package/dist/repo-requests.js.map +1 -0
  46. package/dist/repo-scan.d.ts +47 -0
  47. package/dist/repo-scan.d.ts.map +1 -0
  48. package/dist/repo-scan.js +84 -0
  49. package/dist/repo-scan.js.map +1 -0
  50. package/dist/run-cmd.d.ts +16 -1
  51. package/dist/run-cmd.d.ts.map +1 -1
  52. package/dist/run-cmd.js +68 -9
  53. package/dist/run-cmd.js.map +1 -1
  54. package/dist/serve-cmd.d.ts +17 -0
  55. package/dist/serve-cmd.d.ts.map +1 -1
  56. package/dist/serve-cmd.js +29 -2
  57. package/dist/serve-cmd.js.map +1 -1
  58. package/dist/serve-tasks.d.ts +17 -0
  59. package/dist/serve-tasks.d.ts.map +1 -0
  60. package/dist/serve-tasks.js +124 -0
  61. package/dist/serve-tasks.js.map +1 -0
  62. package/dist/serve.d.ts +36 -1
  63. package/dist/serve.d.ts.map +1 -1
  64. package/dist/serve.js +89 -12
  65. package/dist/serve.js.map +1 -1
  66. package/dist/server-client.d.ts +37 -0
  67. package/dist/server-client.d.ts.map +1 -1
  68. package/dist/server-client.js +21 -0
  69. package/dist/server-client.js.map +1 -1
  70. package/dist/sibling-repos.d.ts +101 -0
  71. package/dist/sibling-repos.d.ts.map +1 -0
  72. package/dist/sibling-repos.js +160 -0
  73. package/dist/sibling-repos.js.map +1 -0
  74. package/dist/sync.d.ts.map +1 -1
  75. package/dist/sync.js +3 -1
  76. package/dist/sync.js.map +1 -1
  77. package/dist/tool-bridge.d.ts +51 -1
  78. package/dist/tool-bridge.d.ts.map +1 -1
  79. package/dist/tool-bridge.js +62 -6
  80. package/dist/tool-bridge.js.map +1 -1
  81. package/dist/tool-protocol.d.ts +21 -3
  82. package/dist/tool-protocol.d.ts.map +1 -1
  83. package/dist/tool-protocol.js.map +1 -1
  84. package/dist/tool-worker.d.ts +13 -1
  85. package/dist/tool-worker.d.ts.map +1 -1
  86. package/dist/tool-worker.js +9 -2
  87. package/dist/tool-worker.js.map +1 -1
  88. package/package.json +2 -2
@@ -15,7 +15,9 @@ const core_1 = require("@looply/core");
15
15
  */
16
16
  async function packetForGoal(goal, read,
17
17
  /** The programme's other goals, so the packet can name what they own (F19). Absent: nothing named. */
18
- siblings = []) {
18
+ siblings = [],
19
+ /** The other REPOSITORIES this goal's change spans, as this machine resolved them. Absent: none. */
20
+ repositories) {
19
21
  const ids = [...new Set((goal.requirements ?? []).map((l) => l.sourceId))].sort();
20
22
  const sources = new Map();
21
23
  const unreadable = [];
@@ -27,6 +29,6 @@ siblings = []) {
27
29
  unreadable.push(id);
28
30
  }
29
31
  }
30
- return { packet: (0, core_1.taskPacketFor)(goal, sources, siblings), unreadable };
32
+ return { packet: (0, core_1.taskPacketFor)(goal, sources, siblings, repositories), unreadable };
31
33
  }
32
34
  //# sourceMappingURL=packet-for-goal.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"packet-for-goal.js","sourceRoot":"","sources":["../src/packet-for-goal.ts"],"names":[],"mappings":";;AAaA,sCAiBC;AA9BD,uCAA4F;AAE5F;;;;;;;;;;GAUG;AACI,KAAK,UAAU,aAAa,CACjC,IAA4D,EAC5D,IAAiD;AACjD,sGAAsG;AACtG,WAAmE,EAAE;IAErE,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAChD,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,IAAA,oBAAa,EAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;AACxE,CAAC"}
1
+ {"version":3,"file":"packet-for-goal.js","sourceRoot":"","sources":["../src/packet-for-goal.ts"],"names":[],"mappings":";;AAaA,sCAmBC;AAhCD,uCAAiH;AAEjH;;;;;;;;;;GAUG;AACI,KAAK,UAAU,aAAa,CACjC,IAA4D,EAC5D,IAAiD;AACjD,sGAAsG;AACtG,WAAmE,EAAE;AACrE,oGAAoG;AACpG,YAA6B;IAE7B,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAChD,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,IAAA,oBAAa,EAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,CAAC,EAAE,UAAU,EAAE,CAAC;AACtF,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { type RepoProfile } from './init-detect';
2
+ /**
3
+ * `looply init`, with every question answered by its default — for a folder
4
+ * nobody is sitting in.
5
+ *
6
+ * ⚠️ THE SAME PATH `init --yes` TAKES, NOT A SECOND DRAFT. `init` with no
7
+ * terminal runs `runSetup({ useDefaults: true })`, which is `DEFAULT_ANSWERS`,
8
+ * and renders with `renderConfig`. `connect`, the supervisor's rescan and the
9
+ * clone worker all go through here, so a repository set up by any of them is
10
+ * byte-for-byte what `looply init` would have drafted in it.
11
+ *
12
+ * 🔴 NEVER OVERWRITES. By the time a config exists it is the person's, full of
13
+ * hand-tuned rails a fresh draft would throw away — the rule `init` keeps.
14
+ */
15
+ export interface QuietInitFs {
16
+ exists(path: string): boolean;
17
+ /** `null` when it cannot be read; detection treats that as absent. */
18
+ readText(path: string): string | null;
19
+ mkdirp(path: string): void;
20
+ writeText(path: string, text: string): void;
21
+ }
22
+ export declare const nodeQuietInitFs: QuietInitFs;
23
+ export type InitOutcome = {
24
+ readonly kind: 'initialised';
25
+ } | {
26
+ readonly kind: 'already';
27
+ } | {
28
+ readonly kind: 'failed';
29
+ readonly reason: string;
30
+ };
31
+ /** What `init` shows before it asks anything; shared so the two read a repository the same way. */
32
+ export declare function profileOf(dir: string, readText: (path: string) => string | null): RepoProfile;
33
+ /** Why `<dir>/.looply/config.yaml` would not load, or null when it would — the daemon's own parser. */
34
+ export declare function configProblemOf(dir: string, readText?: (path: string) => string | null): string | null;
35
+ export declare function initRepoQuietly(dir: string, fs?: QuietInitFs): InitOutcome;
36
+ //# sourceMappingURL=repo-init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-init.d.ts","sourceRoot":"","sources":["../src/repo-init.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAG7D;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACtC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7C;AAED,eAAO,MAAM,eAAe,EAAE,WAc7B,CAAC;AAEF,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;CAAE,GAChC;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,mGAAmG;AACnG,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,WAAW,CAE7F;AAED,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAA+B,GAAG,MAAM,GAAG,IAAI,CAShI;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAE,WAA6B,GAAG,WAAW,CAY3F"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nodeQuietInitFs = void 0;
4
+ exports.profileOf = profileOf;
5
+ exports.configProblemOf = configProblemOf;
6
+ exports.initRepoQuietly = initRepoQuietly;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const config_1 = require("./config");
10
+ const init_detect_1 = require("./init-detect");
11
+ const setup_1 = require("./setup");
12
+ exports.nodeQuietInitFs = {
13
+ exists: (p) => (0, node_fs_1.existsSync)(p),
14
+ readText: (p) => {
15
+ try {
16
+ return (0, node_fs_1.readFileSync)(p, 'utf8');
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ },
22
+ mkdirp: (p) => {
23
+ (0, node_fs_1.mkdirSync)(p, { recursive: true });
24
+ },
25
+ // `wx`: a config that appeared between the check and the write is kept.
26
+ writeText: (p, text) => (0, node_fs_1.writeFileSync)(p, text, { flag: 'wx' }),
27
+ };
28
+ /** What `init` shows before it asks anything; shared so the two read a repository the same way. */
29
+ function profileOf(dir, readText) {
30
+ return (0, init_detect_1.detectRepo)({ readFile: (p) => readText((0, node_path_1.join)(dir, p)) }, (0, node_path_1.basename)(dir));
31
+ }
32
+ /** Why `<dir>/.looply/config.yaml` would not load, or null when it would — the daemon's own parser. */
33
+ function configProblemOf(dir, readText = exports.nodeQuietInitFs.readText) {
34
+ const text = readText((0, node_path_1.join)(dir, '.looply', 'config.yaml'));
35
+ if (text === null)
36
+ return 'it cannot be read';
37
+ try {
38
+ (0, config_1.parseConfig)(text);
39
+ return null;
40
+ }
41
+ catch (e) {
42
+ return e.message;
43
+ }
44
+ }
45
+ function initRepoQuietly(dir, fs = exports.nodeQuietInitFs) {
46
+ const target = (0, node_path_1.join)(dir, '.looply', 'config.yaml');
47
+ try {
48
+ if (fs.exists(target))
49
+ return { kind: 'already' };
50
+ const profile = profileOf(dir, (p) => fs.readText(p));
51
+ fs.mkdirp((0, node_path_1.join)(dir, '.looply'));
52
+ fs.writeText(target, (0, setup_1.renderConfig)(profile, { ...setup_1.DEFAULT_ANSWERS }));
53
+ return { kind: 'initialised' };
54
+ }
55
+ catch (e) {
56
+ if (e.code === 'EEXIST')
57
+ return { kind: 'already' };
58
+ return { kind: 'failed', reason: e.message };
59
+ }
60
+ }
61
+ //# sourceMappingURL=repo-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-init.js","sourceRoot":"","sources":["../src/repo-init.ts"],"names":[],"mappings":";;;AAkDA,8BAEC;AAGD,0CASC;AAED,0CAYC;AA9ED,qCAA6E;AAC7E,yCAA2C;AAC3C,qCAAuC;AACvC,+CAA6D;AAC7D,mCAAwD;AAwB3C,QAAA,eAAe,GAAgB;IAC1C,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAA,oBAAU,EAAC,CAAC,CAAC;IAC5B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;QACd,IAAI,CAAC;YACH,OAAO,IAAA,sBAAY,EAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;QACZ,IAAA,mBAAS,EAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC;IACD,wEAAwE;IACxE,SAAS,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAA,uBAAa,EAAC,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;CAC/D,CAAC;AAOF,mGAAmG;AACnG,SAAgB,SAAS,CAAC,GAAW,EAAE,QAAyC;IAC9E,OAAO,IAAA,wBAAU,EAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAA,gBAAI,EAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,IAAA,oBAAQ,EAAC,GAAG,CAAC,CAAC,CAAC;AAChF,CAAC;AAED,uGAAuG;AACvG,SAAgB,eAAe,CAAC,GAAW,EAAE,WAA4C,uBAAe,CAAC,QAAQ;IAC/G,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAA,gBAAI,EAAC,GAAG,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC;IAC3D,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,mBAAmB,CAAC;IAC9C,IAAI,CAAC;QACH,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAQ,CAAW,CAAC,OAAO,CAAC;IAC9B,CAAC;AACH,CAAC;AAED,SAAgB,eAAe,CAAC,GAAW,EAAE,KAAkB,uBAAe;IAC5E,MAAM,MAAM,GAAG,IAAA,gBAAI,EAAC,GAAG,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC;IACnD,IAAI,CAAC;QACH,IAAI,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAClD,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACtD,EAAE,CAAC,MAAM,CAAC,IAAA,gBAAI,EAAC,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC;QAChC,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,IAAA,oBAAY,EAAC,OAAO,EAAE,EAAE,GAAG,uBAAe,EAAE,CAAC,CAAC,CAAC;QACpE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IACjC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAC/E,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC;IAC1D,CAAC;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { GitExec } from './git-driver';
2
+ /**
3
+ * Where this checkout came from, as the server spells it — or `null`.
4
+ *
5
+ * Asked through the CLI's own git exec (`nodeGitExec`): absolute git binary,
6
+ * hooks off, the `GIT_` namespace rebuilt, and refused outright on a
7
+ * repository whose config names a filter. Reading a remote is still running
8
+ * git in somebody's tree.
9
+ *
10
+ * ⚠️ NULL ON ANY FAILURE, AND NULL IS ORDINARY. No `origin`, a local-path
11
+ * remote, a refused repository: all of them mean "send no URL", never "do not
12
+ * register" — the URL is a convenience for matching, not an identity.
13
+ */
14
+ export declare function readOriginUrl(exec: GitExec): Promise<string | null>;
15
+ //# sourceMappingURL=repo-origin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-origin.d.ts","sourceRoot":"","sources":["../src/repo-origin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQzE"}
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readOriginUrl = readOriginUrl;
4
+ const core_1 = require("@looply/core");
5
+ /**
6
+ * Where this checkout came from, as the server spells it — or `null`.
7
+ *
8
+ * Asked through the CLI's own git exec (`nodeGitExec`): absolute git binary,
9
+ * hooks off, the `GIT_` namespace rebuilt, and refused outright on a
10
+ * repository whose config names a filter. Reading a remote is still running
11
+ * git in somebody's tree.
12
+ *
13
+ * ⚠️ NULL ON ANY FAILURE, AND NULL IS ORDINARY. No `origin`, a local-path
14
+ * remote, a refused repository: all of them mean "send no URL", never "do not
15
+ * register" — the URL is a convenience for matching, not an identity.
16
+ */
17
+ async function readOriginUrl(exec) {
18
+ try {
19
+ const r = await exec(['remote', 'get-url', 'origin']);
20
+ if (r.code !== 0)
21
+ return null;
22
+ return (0, core_1.normaliseRepoUrl)(r.stdout.trim().split(/\r?\n/)[0] ?? '');
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ //# sourceMappingURL=repo-origin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-origin.js","sourceRoot":"","sources":["../src/repo-origin.ts"],"names":[],"mappings":";;AAeA,sCAQC;AAtBD,uCAAgD;AAEhD;;;;;;;;;;;GAWG;AACI,KAAK,UAAU,aAAa,CAAC,IAAa;IAC/C,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC9B,OAAO,IAAA,uBAAgB,EAAC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACnE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,79 @@
1
+ import type { ProvisionRow } from './connect';
2
+ import type { RepoRequest } from './server-client';
3
+ import type { Platform } from './serve';
4
+ /**
5
+ * CLONE WHAT SOMEBODY ASKED THIS MACHINE FOR.
6
+ *
7
+ * A person in AutoRyn (or the app) names a repository by URL and a machine to
8
+ * hold it; the server keeps that as a request until the machine answers. The
9
+ * supervisor asks once a minute, and for each one: clones it under the clone
10
+ * root, sets it up and serves it exactly as `connect` would, and says how it
11
+ * went — `cloned` with the path, or `failed` with a short reason.
12
+ *
13
+ * ⚠️ ONE CLONE AT A TIME, AND NEVER THE SAME REQUEST TWICE AT ONCE. A clone is
14
+ * the one slow, bandwidth-heavy thing the supervisor does; two at once would
15
+ * be two half-finished folders when the network drops. The supervisor never
16
+ * overlaps two runs of this worker, and `inFlight` holds the line inside one.
17
+ *
18
+ * ⚠️ IDEMPOTENT BY LOOKING, NOT BY REMEMBERING. If the answer to the server
19
+ * is lost, the request is still `requested` next minute; the folder is then
20
+ * already a clone of that URL, which is read as "cloned" and answered again.
21
+ * Nothing on disk records what was asked.
22
+ *
23
+ * 🔴 NO CREDENTIALS LEAVE THIS FILE. The URL cloned is the normalised one —
24
+ * userinfo stripped — and a failure is reported as git's first line of
25
+ * complaint with any `user:secret@` cut out, never the whole stderr.
26
+ *
27
+ * ⚠️ AN OLDER SERVER IS NOT A FAULT. A 404 on the route means the feature is
28
+ * not there yet: said once, and asked again quietly every minute in case the
29
+ * server is upgraded under a running supervisor.
30
+ */
31
+ export declare const REPO_REQUEST_POLL_MS = 60000;
32
+ /** `<name>`, `<name>-2` … `<name>-50`, then give up rather than walk for ever. */
33
+ export declare const MAX_NAME_SUFFIX = 50;
34
+ export interface RepoRequestDeps {
35
+ readonly platform: Platform;
36
+ /** `null` when this machine is not paired — nothing to ask with. */
37
+ list(): Promise<readonly RepoRequest[] | null>;
38
+ report(id: string, result: {
39
+ state: 'cloned' | 'failed';
40
+ error?: string;
41
+ path?: string;
42
+ }): Promise<void>;
43
+ cloneRoot(): string;
44
+ exists(path: string): boolean;
45
+ mkdirp(path: string): void;
46
+ /** The raw `origin` URL of a checkout, or null when it is not one (or has none). */
47
+ originOf(dir: string): Promise<string | null>;
48
+ clone(url: string, target: string): Promise<{
49
+ ok: true;
50
+ } | {
51
+ ok: false;
52
+ error: string;
53
+ }>;
54
+ /** `init` (if needed) + `serve add`, as `connect` does it. */
55
+ provision(dir: string): ProvisionRow;
56
+ log(line: string): void;
57
+ }
58
+ export declare class RepoRequestWorker {
59
+ private readonly deps;
60
+ private readonly inFlight;
61
+ private unavailableSaid;
62
+ private lastComplaint;
63
+ private unpairedSaid;
64
+ constructor(deps: RepoRequestDeps);
65
+ run(): Promise<void>;
66
+ private handle;
67
+ private cloneOne;
68
+ }
69
+ /**
70
+ * A folder name from what the server called the repository, or from the URL.
71
+ *
72
+ * 🔴 THE NAME IS INPUT, NOT A PATH. `..\..\Startup` must not become one — so
73
+ * separators, drive colons and the characters Windows refuses are replaced,
74
+ * and leading dots trimmed.
75
+ */
76
+ export declare function folderNameFor(name: string, url: string): string;
77
+ /** First line, credentials cut out, at most 200 characters. */
78
+ export declare function shortError(text: string): string;
79
+ //# sourceMappingURL=repo-requests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-requests.d.ts","sourceRoot":"","sources":["../src/repo-requests.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAC3C,kFAAkF;AAClF,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,oEAAoE;IACpE,IAAI,IAAI,OAAO,CAAC,SAAS,WAAW,EAAE,GAAG,IAAI,CAAC,CAAC;IAC/C,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,KAAK,EAAE,QAAQ,GAAG,QAAQ,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzG,SAAS,IAAI,MAAM,CAAC;IACpB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oFAAoF;IACpF,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzF,8DAA8D;IAC9D,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAAC;IACrC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAMD,qBAAa,iBAAiB;IAMhB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,YAAY,CAAS;gBAEA,IAAI,EAAE,eAAe;IAE5C,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YAsCZ,MAAM;YAkBN,QAAQ;CAwCvB;AAID;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAU/D;AAED,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO/C"}
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RepoRequestWorker = exports.MAX_NAME_SUFFIX = exports.REPO_REQUEST_POLL_MS = void 0;
4
+ exports.folderNameFor = folderNameFor;
5
+ exports.shortError = shortError;
6
+ const node_path_1 = require("node:path");
7
+ const core_1 = require("@looply/core");
8
+ /**
9
+ * CLONE WHAT SOMEBODY ASKED THIS MACHINE FOR.
10
+ *
11
+ * A person in AutoRyn (or the app) names a repository by URL and a machine to
12
+ * hold it; the server keeps that as a request until the machine answers. The
13
+ * supervisor asks once a minute, and for each one: clones it under the clone
14
+ * root, sets it up and serves it exactly as `connect` would, and says how it
15
+ * went — `cloned` with the path, or `failed` with a short reason.
16
+ *
17
+ * ⚠️ ONE CLONE AT A TIME, AND NEVER THE SAME REQUEST TWICE AT ONCE. A clone is
18
+ * the one slow, bandwidth-heavy thing the supervisor does; two at once would
19
+ * be two half-finished folders when the network drops. The supervisor never
20
+ * overlaps two runs of this worker, and `inFlight` holds the line inside one.
21
+ *
22
+ * ⚠️ IDEMPOTENT BY LOOKING, NOT BY REMEMBERING. If the answer to the server
23
+ * is lost, the request is still `requested` next minute; the folder is then
24
+ * already a clone of that URL, which is read as "cloned" and answered again.
25
+ * Nothing on disk records what was asked.
26
+ *
27
+ * 🔴 NO CREDENTIALS LEAVE THIS FILE. The URL cloned is the normalised one —
28
+ * userinfo stripped — and a failure is reported as git's first line of
29
+ * complaint with any `user:secret@` cut out, never the whole stderr.
30
+ *
31
+ * ⚠️ AN OLDER SERVER IS NOT A FAULT. A 404 on the route means the feature is
32
+ * not there yet: said once, and asked again quietly every minute in case the
33
+ * server is upgraded under a running supervisor.
34
+ */
35
+ exports.REPO_REQUEST_POLL_MS = 60_000;
36
+ /** `<name>`, `<name>-2` … `<name>-50`, then give up rather than walk for ever. */
37
+ exports.MAX_NAME_SUFFIX = 50;
38
+ /** The parts of a `ServerError` this worker reads, without importing the class into tests. */
39
+ const statusOf = (e) => typeof e?.status === 'number' ? e.status : null;
40
+ class RepoRequestWorker {
41
+ deps;
42
+ inFlight = new Set();
43
+ unavailableSaid = false;
44
+ lastComplaint = null;
45
+ unpairedSaid = false;
46
+ constructor(deps) {
47
+ this.deps = deps;
48
+ }
49
+ async run() {
50
+ let requests;
51
+ try {
52
+ requests = await this.deps.list();
53
+ }
54
+ catch (e) {
55
+ if (statusOf(e) === 404) {
56
+ if (!this.unavailableSaid) {
57
+ this.deps.log('[serve] clone requests: this server does not offer them yet; still serving, asking again later');
58
+ }
59
+ this.unavailableSaid = true;
60
+ return;
61
+ }
62
+ const msg = `[serve] clone requests: could not ask the server — ${e.message}`;
63
+ // Said once per distinct reason: offline for an hour is one line, not sixty.
64
+ if (msg !== this.lastComplaint)
65
+ this.deps.log(msg);
66
+ this.lastComplaint = msg;
67
+ return;
68
+ }
69
+ if (requests === null) {
70
+ if (!this.unpairedSaid)
71
+ this.deps.log('[serve] clone requests: this machine is not paired; none are asked for');
72
+ this.unpairedSaid = true;
73
+ return;
74
+ }
75
+ this.unavailableSaid = false;
76
+ this.lastComplaint = null;
77
+ this.unpairedSaid = false;
78
+ for (const req of requests) {
79
+ if (req.state !== 'requested' || this.inFlight.has(req.id))
80
+ continue;
81
+ this.inFlight.add(req.id);
82
+ try {
83
+ await this.handle(req);
84
+ }
85
+ finally {
86
+ this.inFlight.delete(req.id);
87
+ }
88
+ }
89
+ }
90
+ async handle(req) {
91
+ let outcome;
92
+ try {
93
+ outcome = await this.cloneOne(req);
94
+ }
95
+ catch (e) {
96
+ outcome = { state: 'failed', error: shortError(e.message) };
97
+ }
98
+ if (outcome.state === 'cloned')
99
+ this.deps.log(`[serve] clone ${req.id}: ready at ${outcome.path}`);
100
+ else
101
+ this.deps.log(`[serve] clone ${req.id}: failed — ${outcome.error}`);
102
+ try {
103
+ await this.deps.report(req.id, outcome);
104
+ }
105
+ catch (e) {
106
+ // The request stays `requested`; next minute the folder is found already
107
+ // cloned and this answer is sent again.
108
+ this.deps.log(`[serve] clone ${req.id}: could not tell the server (${e.message}); will retry`);
109
+ }
110
+ }
111
+ async cloneOne(req) {
112
+ const url = (0, core_1.normaliseRepoUrl)(req.url);
113
+ if (url === null)
114
+ return { state: 'failed', error: 'not an http(s) repository address' };
115
+ const p = pathFor(this.deps.platform);
116
+ const root = this.deps.cloneRoot();
117
+ const name = folderNameFor(req.name, url);
118
+ this.deps.mkdirp(root);
119
+ let target = null;
120
+ for (let n = 1; n <= exports.MAX_NAME_SUFFIX && target === null; n += 1) {
121
+ const candidate = p.join(root, n === 1 ? name : `${name}-${n}`);
122
+ if (!this.deps.exists(candidate)) {
123
+ this.deps.log(`[serve] clone ${req.id}: cloning ${url} into ${candidate}`);
124
+ const cloned = await this.deps.clone(url, candidate);
125
+ if (!cloned.ok)
126
+ return { state: 'failed', error: shortError(cloned.error) };
127
+ target = candidate;
128
+ break;
129
+ }
130
+ // Already there: the same repository (a lost answer, or a person who
131
+ // cloned it by hand) is the clone; anything else is in the way.
132
+ const origin = await this.deps.originOf(candidate);
133
+ if (origin !== null && (0, core_1.normaliseRepoUrl)(origin) === url)
134
+ target = candidate;
135
+ }
136
+ if (target === null) {
137
+ return { state: 'failed', error: `${p.join(root, name)} and ${exports.MAX_NAME_SUFFIX - 1} numbered alternatives are taken` };
138
+ }
139
+ const row = this.deps.provision(target);
140
+ if (row.init === 'failed') {
141
+ return { state: 'failed', error: shortError(`cloned, but could not set it up: ${row.reason ?? 'unknown'}`), path: target };
142
+ }
143
+ // ⚠️ A DRAFT THAT NEEDS A PERSON IS STILL A CLONE. A repository with no
144
+ // detected gate gets a config the daemon refuses until somebody adds one;
145
+ // the clone itself succeeded, and saying `failed` would send them looking
146
+ // in the wrong place.
147
+ if (row.served === 'not')
148
+ this.deps.log(`[serve] clone ${req.id}: not served yet — ${row.reason ?? 'unknown'}`);
149
+ return { state: 'cloned', path: target };
150
+ }
151
+ }
152
+ exports.RepoRequestWorker = RepoRequestWorker;
153
+ const pathFor = (platform) => (platform === 'win32' ? node_path_1.win32 : node_path_1.posix);
154
+ /**
155
+ * A folder name from what the server called the repository, or from the URL.
156
+ *
157
+ * 🔴 THE NAME IS INPUT, NOT A PATH. `..\..\Startup` must not become one — so
158
+ * separators, drive colons and the characters Windows refuses are replaced,
159
+ * and leading dots trimmed.
160
+ */
161
+ function folderNameFor(name, url) {
162
+ const clean = (s) => s
163
+ .replace(/[\\/:*?"<>|\u0000-\u001f]/g, '-')
164
+ .replace(/^[.\s-]+|[.\s]+$/g, '')
165
+ .slice(0, 100);
166
+ const fromName = clean(name ?? '');
167
+ if (fromName !== '')
168
+ return fromName;
169
+ const last = url.split('/').filter(Boolean).pop() ?? '';
170
+ return clean(last) || 'repository';
171
+ }
172
+ /** First line, credentials cut out, at most 200 characters. */
173
+ function shortError(text) {
174
+ const line = text
175
+ .split(/\r?\n/)
176
+ .map((l) => l.trim())
177
+ .find((l) => l !== '') ?? 'unknown error';
178
+ return line.replace(/([a-z][a-z0-9+.-]*:\/\/)[^/\s@]*@/gi, '$1').slice(0, 200);
179
+ }
180
+ //# sourceMappingURL=repo-requests.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-requests.js","sourceRoot":"","sources":["../src/repo-requests.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Find the git repositories under a folder a person said holds their code.
3
+ *
4
+ * ⚠️ BOUNDED THREE WAYS, because the folder is whatever they typed and that is
5
+ * sometimes their whole home directory:
6
+ *
7
+ * depth three levels below the root, no further — `~/code/client/app`
8
+ * is found, `~/code/a/b/c/d` is not;
9
+ * pruning `node_modules`, `dist`, `build`, `.next`, `vendor` and every
10
+ * hidden folder are never entered: they are where the walk would
11
+ * spend its time and they hold nobody's project;
12
+ * a cap two hundred repositories, and the caller says it stopped.
13
+ *
14
+ * 🔴 A REPOSITORY IS A LEAF. Once a folder has `.git` (a directory, or the
15
+ * file a linked worktree or submodule has) the walk does not go inside it: a
16
+ * vendored checkout or a submodule is part of the project around it, not a
17
+ * second project to serve.
18
+ *
19
+ * Symlinks are not followed — a link back up the tree is a walk that never ends.
20
+ */
21
+ export declare const SCAN_MAX_DEPTH = 3;
22
+ export declare const SCAN_MAX_REPOS = 200;
23
+ export declare const SCAN_SKIP: Set<string>;
24
+ export interface ScanEntry {
25
+ readonly name: string;
26
+ /** A real directory — not a symlink to one. */
27
+ readonly dir: boolean;
28
+ }
29
+ export interface ScanFs {
30
+ /** The entries of a folder, or `null` when it cannot be read. */
31
+ entries(dir: string): readonly ScanEntry[] | null;
32
+ }
33
+ export declare const nodeScanFs: ScanFs;
34
+ export interface ScanResult {
35
+ /** Repository roots, in walk order (sorted by name at each level). */
36
+ readonly repos: readonly string[];
37
+ /** True when the cap was reached and the walk stopped early. */
38
+ readonly capped: boolean;
39
+ /** Folders that could not be listed — said, not silently skipped. */
40
+ readonly unreadable: readonly string[];
41
+ }
42
+ export declare function scanForRepos(roots: readonly string[], fs: ScanFs, opts?: {
43
+ maxDepth?: number;
44
+ cap?: number;
45
+ join?: (a: string, b: string) => string;
46
+ }): ScanResult;
47
+ //# sourceMappingURL=repo-scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-scan.d.ts","sourceRoot":"","sources":["../src/repo-scan.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,SAAS,aAAgE,CAAC;AAEvF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,MAAM;IACrB,iEAAiE;IACjE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,GAAG,IAAI,CAAC;CACnD;AAED,eAAO,MAAM,UAAU,EAAE,MAQxB,CAAC;AAEF,MAAM,WAAW,UAAU;IACzB,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,EAAE,EAAE,MAAM,EACV,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;CAAO,GACtF,UAAU,CAwCZ"}
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nodeScanFs = exports.SCAN_SKIP = exports.SCAN_MAX_REPOS = exports.SCAN_MAX_DEPTH = void 0;
4
+ exports.scanForRepos = scanForRepos;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ /**
8
+ * Find the git repositories under a folder a person said holds their code.
9
+ *
10
+ * ⚠️ BOUNDED THREE WAYS, because the folder is whatever they typed and that is
11
+ * sometimes their whole home directory:
12
+ *
13
+ * depth three levels below the root, no further — `~/code/client/app`
14
+ * is found, `~/code/a/b/c/d` is not;
15
+ * pruning `node_modules`, `dist`, `build`, `.next`, `vendor` and every
16
+ * hidden folder are never entered: they are where the walk would
17
+ * spend its time and they hold nobody's project;
18
+ * a cap two hundred repositories, and the caller says it stopped.
19
+ *
20
+ * 🔴 A REPOSITORY IS A LEAF. Once a folder has `.git` (a directory, or the
21
+ * file a linked worktree or submodule has) the walk does not go inside it: a
22
+ * vendored checkout or a submodule is part of the project around it, not a
23
+ * second project to serve.
24
+ *
25
+ * Symlinks are not followed — a link back up the tree is a walk that never ends.
26
+ */
27
+ exports.SCAN_MAX_DEPTH = 3;
28
+ exports.SCAN_MAX_REPOS = 200;
29
+ exports.SCAN_SKIP = new Set(['node_modules', 'dist', 'build', '.next', 'vendor']);
30
+ exports.nodeScanFs = {
31
+ entries: (dir) => {
32
+ try {
33
+ return (0, node_fs_1.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ },
39
+ };
40
+ function scanForRepos(roots, fs, opts = {}) {
41
+ const maxDepth = opts.maxDepth ?? exports.SCAN_MAX_DEPTH;
42
+ const cap = opts.cap ?? exports.SCAN_MAX_REPOS;
43
+ const joinPath = opts.join ?? node_path_1.join;
44
+ const repos = [];
45
+ const unreadable = [];
46
+ const seen = new Set();
47
+ let capped = false;
48
+ const walk = (dir, depth) => {
49
+ if (capped)
50
+ return;
51
+ const entries = fs.entries(dir);
52
+ if (entries === null) {
53
+ unreadable.push(dir);
54
+ return;
55
+ }
56
+ // `.git` as a folder OR a file: a linked worktree's is a file.
57
+ if (entries.some((e) => e.name === '.git')) {
58
+ if (seen.has(dir))
59
+ return;
60
+ if (repos.length >= cap) {
61
+ capped = true;
62
+ return;
63
+ }
64
+ seen.add(dir);
65
+ repos.push(dir);
66
+ return;
67
+ }
68
+ if (depth >= maxDepth)
69
+ return;
70
+ const children = entries
71
+ .filter((e) => e.dir && !e.name.startsWith('.') && !exports.SCAN_SKIP.has(e.name.toLowerCase()))
72
+ .map((e) => e.name)
73
+ .sort((a, b) => a.localeCompare(b));
74
+ for (const name of children) {
75
+ if (capped)
76
+ return;
77
+ walk(joinPath(dir, name), depth + 1);
78
+ }
79
+ };
80
+ for (const root of roots)
81
+ walk(root, 0);
82
+ return { repos, capped, unreadable };
83
+ }
84
+ //# sourceMappingURL=repo-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-scan.js","sourceRoot":"","sources":["../src/repo-scan.ts"],"names":[],"mappings":"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"}
package/dist/run-cmd.d.ts CHANGED
@@ -5,6 +5,7 @@ import { type AnyEventInput } from './ledger-store';
5
5
  import { type RepairContext, type RunSummary } from './round-runner';
6
6
  import type { AnsweredQuestionView, PartialQuestionBody } from '@looply/core';
7
7
  import { type ApprovalResolutionContext } from './approval-resolution';
8
+ import { type RepositorySpan } from '@looply/core';
8
9
  import { type GovernedActionResultContext } from './governed-run';
9
10
  /**
10
11
  * The composition root for `looply run`.
@@ -323,7 +324,21 @@ repair?: RepairContext,
323
324
  * trim here would be a second place the budget lives — the same reason
324
325
  * `memory` is neither ranked nor trimmed by this function.
325
326
  */
326
- goal?: string | null): string;
327
+ goal?: string | null,
328
+ /**
329
+ * 🔴 WHICH PART OF A MULTI-REPOSITORY CHANGE THIS REPOSITORY IS.
330
+ *
331
+ * The goal's words are the same in every repository the change spans — an
332
+ * external system sets one goal per repository with one outcome text — so
333
+ * without this a round reads "add delivery tips" as the whole job, and an app
334
+ * round invents the backend route it was meant to call. The span names this
335
+ * repository's part, the others' labels, and where each can be READ.
336
+ *
337
+ * Rendered by core's `renderRepositorySpan`, the same lines the planner's
338
+ * packet carries, so the round and the plan are told one thing. Absent for a
339
+ * goal naming no sibling: the prompt is then byte for byte what it was.
340
+ */
341
+ span?: RepositorySpan): string;
327
342
  /**
328
343
  * A way to re-read the goal mirror from the server, if this machine has one.
329
344
  *