@north-light/crouter 0.3.164 → 0.3.166

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 (160) hide show
  1. package/dist/build-root.d.ts +6 -6
  2. package/dist/build-root.js +12 -13
  3. package/dist/builtin-memory/00-runtime-base.md +3 -4
  4. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  5. package/dist/builtin-memory/02-lifecycle/00-terminal.md +2 -0
  6. package/dist/builtin-memory/04-orchestration-kernel.md +16 -18
  7. package/dist/builtin-memory/05-kinds/advisor/00-base.md +0 -2
  8. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -9
  9. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -3
  10. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -10
  11. package/dist/builtin-memory/05-kinds/explore/00-base.md +2 -2
  12. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +0 -2
  13. package/dist/builtin-memory/05-kinds/general/00-base.md +3 -5
  14. package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -4
  15. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -3
  16. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +12 -0
  17. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -2
  18. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -2
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -2
  20. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -1
  21. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -2
  22. package/dist/builtin-memory/05-kinds/review/00-base.md +0 -2
  23. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -3
  24. package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
  25. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +2 -4
  26. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -1
  27. package/dist/builtin-memory/advisor/council.md +40 -0
  28. package/dist/builtin-memory/design.md +8 -11
  29. package/dist/builtin-memory/development.md +6 -9
  30. package/dist/builtin-memory/internal/INDEX.md +4 -5
  31. package/dist/builtin-memory/internal/agent-shaping.md +5 -7
  32. package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
  33. package/dist/builtin-memory/internal/marketplaces.md +13 -14
  34. package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
  35. package/dist/builtin-memory/internal/plugins.md +61 -45
  36. package/dist/builtin-memory/planning.md +4 -7
  37. package/dist/builtin-memory/spec.md +9 -12
  38. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +8 -22
  39. package/dist/cli.js +1 -1
  40. package/dist/clients/attach/__tests__/attach-keybindings.test.js +17 -4
  41. package/dist/clients/attach/__tests__/context-message.test.js +62 -22
  42. package/dist/clients/attach/__tests__/crtr-output.test.js +39 -30
  43. package/dist/clients/attach/render/chat-view.d.ts +20 -20
  44. package/dist/clients/attach/render/chat-view.js +40 -54
  45. package/dist/clients/attach/render/{frozen-history.d.ts → condensed-history.d.ts} +1 -18
  46. package/dist/clients/attach/render/condensed-history.js +60 -0
  47. package/dist/clients/attach/render/context-message.d.ts +1 -1
  48. package/dist/clients/attach/render/context-message.js +18 -41
  49. package/dist/clients/attach/session/bindings.d.ts +5 -3
  50. package/dist/clients/attach/session/bindings.js +7 -13
  51. package/dist/clients/attach/session/keys.d.ts +2 -0
  52. package/dist/clients/attach/session/keys.js +36 -37
  53. package/dist/clients/attach/session/profile-files.d.ts +8 -0
  54. package/dist/clients/attach/session/profile-files.js +157 -0
  55. package/dist/clients/attach/viewer.js +528 -529
  56. package/dist/commands/node-context.js +3 -2
  57. package/dist/commands/node.js +2 -2
  58. package/dist/commands/pkg/plugin-inspect.js +6 -7
  59. package/dist/commands/pkg/plugin-manage.d.ts +1 -1
  60. package/dist/commands/pkg/plugin-manage.js +131 -19
  61. package/dist/commands/pkg/plugin.js +2 -2
  62. package/dist/commands/pkg.js +6 -11
  63. package/dist/commands/profile/env.js +3 -3
  64. package/dist/commands/sys/config.js +17 -76
  65. package/dist/commands/sys/doctor.js +5 -91
  66. package/dist/commands/sys/setup-wizard.d.ts +9 -11
  67. package/dist/commands/sys/setup-wizard.js +47 -81
  68. package/dist/core/__tests__/base-worker-prompt.test.js +18 -21
  69. package/dist/core/__tests__/command-plugins-surfaces.test.js +39 -5
  70. package/dist/core/__tests__/command-plugins.test.js +75 -16
  71. package/dist/core/__tests__/review-model-floor.test.js +2 -2
  72. package/dist/core/__tests__/tmux-surface.test.js +10 -1
  73. package/dist/core/command-manifests/manifest.d.ts +24 -0
  74. package/dist/core/{configured-clis → command-manifests}/manifest.js +28 -25
  75. package/dist/core/command-manifests/registry.d.ts +1 -2
  76. package/dist/core/command-manifests/registry.js +2 -2
  77. package/dist/core/command-manifests/schema.d.ts +11 -13
  78. package/dist/core/command-manifests/schema.js +53 -192
  79. package/dist/core/command-plugins/compose.d.ts +0 -6
  80. package/dist/core/command-plugins/compose.js +32 -75
  81. package/dist/core/command-plugins/discovery.d.ts +25 -58
  82. package/dist/core/command-plugins/discovery.js +171 -261
  83. package/dist/core/command-plugins/endpoint.d.ts +24 -0
  84. package/dist/core/command-plugins/endpoint.js +48 -0
  85. package/dist/core/command-plugins/store.d.ts +16 -0
  86. package/dist/core/command-plugins/store.js +64 -0
  87. package/dist/core/command-plugins/{adapter.d.ts → transport/exec-invoke.d.ts} +3 -3
  88. package/dist/core/command-plugins/{adapter.js → transport/exec-invoke.js} +4 -4
  89. package/dist/core/{configured-clis/fetch.d.ts → command-plugins/transport/http-fetch.d.ts} +9 -18
  90. package/dist/core/{configured-clis/fetch.js → command-plugins/transport/http-fetch.js} +15 -35
  91. package/dist/core/{configured-clis/invoker.d.ts → command-plugins/transport/http-invoke.d.ts} +7 -7
  92. package/dist/core/{configured-clis/invoker.js → command-plugins/transport/http-invoke.js} +21 -23
  93. package/dist/core/command.d.ts +8 -9
  94. package/dist/core/command.js +6 -9
  95. package/dist/core/config.js +6 -10
  96. package/dist/core/env-name.d.ts +6 -0
  97. package/dist/core/env-name.js +9 -0
  98. package/dist/core/io.d.ts +1 -1
  99. package/dist/core/keybindings/__tests__/resolve.test.js +40 -3
  100. package/dist/core/keybindings/attach-control.d.ts +37 -0
  101. package/dist/core/keybindings/attach-control.js +38 -0
  102. package/dist/core/keybindings/catalog.d.ts +5 -4
  103. package/dist/core/keybindings/catalog.js +16 -8
  104. package/dist/core/keybindings/index.d.ts +1 -0
  105. package/dist/core/keybindings/index.js +1 -0
  106. package/dist/core/keybindings/types.d.ts +1 -1
  107. package/dist/core/preview-registry.js +41 -74
  108. package/dist/core/runtime/bearings.d.ts +2 -5
  109. package/dist/core/runtime/bearings.js +2 -5
  110. package/dist/core/runtime/broker.js +3 -2
  111. package/dist/core/runtime/front-door.d.ts +1 -1
  112. package/dist/core/runtime/front-door.js +2 -2
  113. package/dist/core/runtime/kickoff.d.ts +3 -3
  114. package/dist/core/runtime/kickoff.js +4 -3
  115. package/dist/core/runtime/lifecycle.js +2 -2
  116. package/dist/core/runtime/situational-context.d.ts +1 -1
  117. package/dist/core/runtime/situational-context.js +1 -1
  118. package/dist/core/runtime/spawn.js +9 -9
  119. package/dist/core/runtime/tmux.js +29 -2
  120. package/dist/core/scope.d.ts +0 -5
  121. package/dist/core/scope.js +0 -10
  122. package/dist/core/user-settings.d.ts +193 -0
  123. package/dist/core/user-settings.js +252 -0
  124. package/dist/index.d.ts +4 -0
  125. package/dist/index.js +3 -0
  126. package/dist/pi-extensions/canvas-stophook.js +3 -2
  127. package/dist/prompts/review.js +4 -2
  128. package/dist/shared/generated-context.d.ts +34 -0
  129. package/dist/shared/generated-context.js +98 -0
  130. package/dist/types.d.ts +23 -16
  131. package/dist/types.js +3 -14
  132. package/dist/web-client/assets/index-BgLGlZ3D.css +2 -0
  133. package/dist/web-client/assets/{index-NIuSCOHM.js → index-CmoNqcCv.js} +19 -19
  134. package/dist/web-client/index.html +2 -2
  135. package/dist/web-client/sw.js +1 -1
  136. package/docs/public-api.md +1 -0
  137. package/package.json +1 -1
  138. package/runtime.lock.json +2 -2
  139. package/dist/builtin-memory/05-kinds/product/00-base.md +0 -25
  140. package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +0 -15
  141. package/dist/builtin-memory/05-kinds/product/teardown.md +0 -15
  142. package/dist/builtin-memory/internal/workflow-codification.md +0 -82
  143. package/dist/builtin-memory/product.md +0 -80
  144. package/dist/clients/attach/render/frozen-history.js +0 -100
  145. package/dist/commands/pkg/cli-inspect.d.ts +0 -17
  146. package/dist/commands/pkg/cli-inspect.js +0 -190
  147. package/dist/commands/pkg/cli-manage.d.ts +0 -3
  148. package/dist/commands/pkg/cli-manage.js +0 -206
  149. package/dist/commands/pkg/cli.d.ts +0 -1
  150. package/dist/commands/pkg/cli.js +0 -14
  151. package/dist/core/configured-clis/cache.d.ts +0 -16
  152. package/dist/core/configured-clis/cache.js +0 -57
  153. package/dist/core/configured-clis/compose.d.ts +0 -14
  154. package/dist/core/configured-clis/compose.js +0 -60
  155. package/dist/core/configured-clis/discovery.d.ts +0 -47
  156. package/dist/core/configured-clis/discovery.js +0 -173
  157. package/dist/core/configured-clis/manifest.d.ts +0 -24
  158. package/dist/core/configured-clis/registration.d.ts +0 -40
  159. package/dist/core/configured-clis/registration.js +0 -201
  160. package/dist/web-client/assets/index-CqLKj8Xu.css +0 -2
@@ -1,173 +0,0 @@
1
- // Configured-CLI discovery — the single integration seam that joins effective
2
- // registrations, their STORED manifest bytes, pure §4 validation, and configured
3
- // contribution creation, then folds the result together with command-plugin
4
- // candidates into ONE unified, collision-resolved command snapshot.
5
- //
6
- // This is the serial integration point (E1). Three consumers read the exact same
7
- // final snapshot so their view of accepted commands and issues can never drift:
8
- // - dispatch / root composition (build-root.ts) via `buildExternalCommandSnapshot`
9
- // - `pkg cli show` + `pkg plugin show` (lifecycle inspection)
10
- // - `sys doctor`
11
- //
12
- // Two hard invariants of the amended spec:
13
- // 1. Composition from a stored manifest does ZERO network and ZERO freshness
14
- // work — no TTL, no ETag, no revalidation, no debounce. The bytes on disk
15
- // are authoritative until a `register`/`refresh` replaces them.
16
- // 2. The ONLY network this module can trigger is `hydrateAbsentConfiguredClis`,
17
- // the one-time absent-store hydration on an unknown-first-token miss (§5.1).
18
- // It is exported separately so dispatch (E2) can call it explicitly; nothing
19
- // on the composition path ever fetches.
20
- import { resolveCommandRegistry } from '../command-manifests/registry.js';
21
- import { adaptPluginContributions } from '../command-plugins/compose.js';
22
- import { discoverPluginCommandCandidates } from '../command-plugins/discovery.js';
23
- import { validateConfiguredCliManifest } from './manifest.js';
24
- import { effectiveConfiguredClis } from './registration.js';
25
- import { isCacheMetadataValid, readConfiguredCliCacheManifest, writeConfiguredCliCacheManifest } from './cache.js';
26
- import { fetchConfiguredCliManifest } from './fetch.js';
27
- import { adaptConfiguredContributions } from './compose.js';
28
- function contributorRef(registration) {
29
- return { kind: 'configured-cli', name: registration.name, scope: registration.scope };
30
- }
31
- /** Read + validate a registration's STORED manifest. Never fetches. A missing,
32
- * metadata-mismatched, or unparseable manifest reads as `absent` (§3.3: corrupt
33
- * or incomplete stored state reads as absent, making it hydration-eligible). A
34
- * parseable manifest that fails §4 validation is `invalid` (stored, reports
35
- * issues, composes nothing — NOT hydration-eligible). */
36
- function readStoredManifest(registration, reservedCoreNames) {
37
- if (!isCacheMetadataValid(registration))
38
- return { state: 'absent' };
39
- const bytes = readConfiguredCliCacheManifest(registration);
40
- if (bytes === null)
41
- return { state: 'absent' };
42
- let raw;
43
- try {
44
- raw = JSON.parse(Buffer.from(bytes).toString('utf8'));
45
- }
46
- catch {
47
- return { state: 'absent' }; // unparseable → reads as absent
48
- }
49
- const validation = validateConfiguredCliManifest(raw, { reservedCoreNames });
50
- if (validation.manifest !== undefined)
51
- return { state: 'valid', manifest: validation.manifest };
52
- return {
53
- state: 'invalid',
54
- issues: validation.issues.map((i) => ({ ...i, contributor: contributorRef(registration) })),
55
- };
56
- }
57
- /** Discover configured-CLI candidates from stored manifests only — zero network.
58
- * Each effective registration (project over user) contributes its valid roots,
59
- * reports its invalid-manifest issues, or is noted as absent-store. */
60
- export function discoverConfiguredCliCandidates(reservedCoreNames) {
61
- const candidates = [];
62
- const issues = [];
63
- const absentStoreNames = [];
64
- for (const registration of effectiveConfiguredClis()) {
65
- const stored = readStoredManifest(registration, reservedCoreNames);
66
- if (stored.state === 'absent') {
67
- absentStoreNames.push(registration.name);
68
- }
69
- else if (stored.state === 'invalid') {
70
- issues.push(...stored.issues);
71
- }
72
- else {
73
- candidates.push(...adaptConfiguredContributions(registration, stored.manifest));
74
- }
75
- }
76
- return { candidates, issues, absentStoreNames };
77
- }
78
- // ---------------------------------------------------------------------------
79
- // The one unified snapshot (zero network)
80
- // ---------------------------------------------------------------------------
81
- /** Convert command-plugin candidates + their per-plugin structural issues into
82
- * the transport-neutral shape, WITHOUT the plugin module's own collision pass
83
- * (collisions are resolved once, uniformly, by `resolveCommandRegistry`). */
84
- function pluginCandidates() {
85
- const validations = discoverPluginCommandCandidates();
86
- // Reuse the plugin adapter purely to bind each ValidatedContribution's leaf to
87
- // its executable-spawn `run`; the registry does the collision resolution.
88
- const candidates = adaptPluginContributions(validations.flatMap((v) => v.contributions)).contributions;
89
- const issues = validations.flatMap((v) => v.issues.map((i) => ({
90
- code: i.code,
91
- ...(i.path !== undefined ? { path: i.path } : {}),
92
- message: i.message,
93
- received: i.received,
94
- expected: i.expected,
95
- next: i.next,
96
- // Command plugins are never installed at builtin scope.
97
- contributor: { kind: 'plugin', name: v.plugin.name, scope: v.plugin.scope },
98
- })));
99
- return { candidates, issues };
100
- }
101
- /** Build THE unified external-command snapshot: every effective command plugin
102
- * and every effective configured CLI, resolved through ONE collision pass, with
103
- * every issue (plugin-structural, configured-manifest-invalid, and cross-
104
- * contributor collision) merged into one list. Performs zero network I/O — all
105
- * manifests read from disk. This exact result is consumed by dispatch/root
106
- * composition, `pkg cli show`/`pkg plugin show`, and `sys doctor`, so their
107
- * accepted-command and issue views can never diverge. `reservedCoreNames` are
108
- * the core top-level subtree names — core always wins. */
109
- export function buildExternalCommandSnapshot(reservedCoreNames) {
110
- const plugins = pluginCandidates();
111
- const configured = discoverConfiguredCliCandidates(reservedCoreNames);
112
- const resolved = resolveCommandRegistry([...plugins.candidates, ...configured.candidates], reservedCoreNames);
113
- return {
114
- contributions: resolved.contributions,
115
- issues: [...plugins.issues, ...configured.issues, ...resolved.issues],
116
- };
117
- }
118
- /** Is this registration's stored manifest absent (hydration-eligible)? Mirrors
119
- * `readStoredManifest`'s absent branch without running the full §4 validator —
120
- * a parseable-but-invalid manifest is stored, not absent. */
121
- function isAbsentStore(registration) {
122
- if (!isCacheMetadataValid(registration))
123
- return true;
124
- const bytes = readConfiguredCliCacheManifest(registration);
125
- if (bytes === null)
126
- return true;
127
- try {
128
- JSON.parse(Buffer.from(bytes).toString('utf8'));
129
- return false;
130
- }
131
- catch {
132
- return true; // unparseable → reads as absent
133
- }
134
- }
135
- /** Hydrate EVERY effective absent-store registration exactly once (§5.1). For
136
- * each registration with no readable stored manifest, perform one authenticated
137
- * `GET endpoint`, storing the exact bytes on success. A fetch failure (network,
138
- * non-2xx, or named-but-empty auth env) leaves the store absent and records a
139
- * one-line diagnostic — it never throws and never earns NETWORK precedence over
140
- * the caller's `unknown_path`/USAGE classification. Registrations that already
141
- * hold a stored manifest (valid OR merely schema-invalid) are skipped, so with
142
- * every manifest stored this performs zero network I/O.
143
- *
144
- * Intended caller: dispatch (E2), on an unknown-first-token miss, followed by a
145
- * single tree rebuild + re-resolve when `hydratedAny` is true. */
146
- export async function hydrateAbsentConfiguredClis() {
147
- let hydratedAny = false;
148
- const diagnostics = [];
149
- for (const registration of effectiveConfiguredClis()) {
150
- if (!isAbsentStore(registration))
151
- continue;
152
- // A per-registration fetch OR store failure (network, non-2xx, empty auth
153
- // env, or a filesystem write error) is a FAILED hydration for this one
154
- // registration: record a diagnostic and continue. It must never throw and
155
- // never earn NETWORK precedence over the caller's unknown_path/USAGE
156
- // outcome (§5.1), so the whole attempt — including the atomic store — is
157
- // caught here.
158
- try {
159
- const result = await fetchConfiguredCliManifest(registration);
160
- if (result.status === 200) {
161
- writeConfiguredCliCacheManifest(registration, result.raw);
162
- hydratedAny = true;
163
- }
164
- else {
165
- diagnostics.push(`configured CLI "${registration.name}": ${result.message} — run \`crtr pkg cli refresh ${registration.name}\``);
166
- }
167
- }
168
- catch (error) {
169
- diagnostics.push(`configured CLI "${registration.name}": ${error.message} — run \`crtr pkg cli refresh ${registration.name}\``);
170
- }
171
- }
172
- return { hydratedAny, diagnostics };
173
- }
@@ -1,24 +0,0 @@
1
- import type { ConfiguredCliDeclLeaf, DeclBranch, ManifestTimeouts, CommandManifestIssue } from '../command-manifests/schema.js';
2
- export interface ValidatedConfiguredCliManifest {
3
- schemaVersion: 1;
4
- baseUrl?: string;
5
- timeouts?: ManifestTimeouts;
6
- /** Top-level branches, each ready to mount at parent []. */
7
- roots: DeclBranch<ConfiguredCliDeclLeaf>[];
8
- }
9
- export interface ConfiguredCliManifestValidation {
10
- /** The validated, materialized manifest (present only if all issues are fixed). */
11
- manifest?: ValidatedConfiguredCliManifest;
12
- /** All typed validation issues. Any issue means zero contributions. */
13
- issues: CommandManifestIssue[];
14
- }
15
- /**
16
- * Validates a configured CLI manifest exactly per §4, performing strict atomic
17
- * validation and order-independent forest materialization. Returns either a
18
- * valid manifest with roots ready to mount, or a complete issues list with
19
- * zero contributions.
20
- */
21
- export declare function validateConfiguredCliManifest(raw: unknown, options: {
22
- reservedCoreNames: ReadonlySet<string>;
23
- }): ConfiguredCliManifestValidation;
24
- export { validateConfiguredCliNode } from '../command-manifests/schema.js';
@@ -1,40 +0,0 @@
1
- import type { Scope } from '../../types.js';
2
- /** A registered configured CLI: name, endpoint, optional auth env var, scope and root. */
3
- export interface ConfiguredCliRegistration {
4
- name: string;
5
- endpoint: string;
6
- authEnvVar?: string;
7
- scope: 'project' | 'user';
8
- root: string;
9
- }
10
- /** Validation result for registration inputs. */
11
- export interface RegistrationValidation {
12
- isValid: boolean;
13
- errors: string[];
14
- }
15
- /** Validate a registration name. */
16
- export declare function validateRegistrationName(name: unknown): RegistrationValidation;
17
- /** Validate an environment variable name. */
18
- export declare function validateEnvVarName(name: unknown): RegistrationValidation;
19
- /** Validate an endpoint URL. */
20
- export declare function validateEndpoint(endpoint: unknown): RegistrationValidation;
21
- /** Normalize endpoint URL to a canonical form (remove trailing slash if present). */
22
- export declare function normalizeEndpoint(endpoint: string): string;
23
- /** List every EFFECTIVE configured CLI visible across scopes (project nearest-first,
24
- * then user). Dedupes by name: the first-seen registration for a name wins (nearer
25
- * project root, then user) and any same-name registration in a farther scope is
26
- * omitted entirely — there is no shadowed-copy representation and no `effective`
27
- * field on `ConfiguredCliRegistration`. */
28
- export declare function listConfiguredCliRegistrations(): ConfiguredCliRegistration[];
29
- /** Get deduplicated effective registrations (one per unique name, project wins over user). */
30
- export declare function effectiveConfiguredClis(): ConfiguredCliRegistration[];
31
- /** Resolve a configured CLI registration by name. If scope is omitted, returns the
32
- * effective winner (project > user). If scope is explicit, returns the registration
33
- * in that scope or null if not found. */
34
- export declare function resolveConfiguredCli(name: string, scope?: Scope | 'project' | 'user'): ConfiguredCliRegistration | null;
35
- /** Get the cache directory for a configured CLI registration. */
36
- export declare function configuredCliCacheDir(registration: ConfiguredCliRegistration): string;
37
- /** Get the manifest file path for a configured CLI. */
38
- export declare function configuredCliManifestPath(registration: ConfiguredCliRegistration): string;
39
- /** Get the cache metadata file path for a configured CLI. */
40
- export declare function configuredCliCacheMetaPath(registration: ConfiguredCliRegistration): string;
@@ -1,201 +0,0 @@
1
- import { join } from 'node:path';
2
- import { configuredClisDir, configuredClisDirForRoot, projectScopeRoots, userScopeRoot } from '../scope.js';
3
- import { readConfig, readConfigAtRoot } from '../config.js';
4
- /** Kebab-case identifier: lowercase letter start, alphanumeric + hyphen chars,
5
- * no trailing hyphens. Examples: `demo`, `my-cli`, `azure-devops`. */
6
- function isValidRegistrationName(name) {
7
- return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name);
8
- }
9
- /** Environment variable name: must start with letter or underscore, followed by
10
- * alphanumeric or underscore chars. Examples: `NL_CORE_TOKEN`, `_INTERNAL`. */
11
- function isValidEnvVarName(name) {
12
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
13
- }
14
- /** Loopback hostnames permitted for plain-http endpoints (local dev only).
15
- * Includes `host.docker.internal`, Docker's own DNS alias a container uses to
16
- * reach its host — the container-network equivalent of loopback for the same
17
- * local-dev trust boundary (e.g. a Dockerized guest registering its host
18
- * Core as a configured CLI). */
19
- function isLoopbackHostname(hostname) {
20
- // URL normalizes IPv6 hosts to bracketed form, e.g. `[::1]`.
21
- return (hostname === 'localhost' ||
22
- hostname === '127.0.0.1' ||
23
- hostname === '[::1]' ||
24
- hostname === '::1' ||
25
- hostname === 'host.docker.internal');
26
- }
27
- /** Validate that endpoint is an absolute HTTPS URL (or HTTP on a loopback host)
28
- * with no credentials/fragment. */
29
- function isValidEndpoint(endpoint) {
30
- try {
31
- const url = new URL(endpoint);
32
- // https required; plain http permitted only for loopback hosts (local dev)
33
- if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopbackHostname(url.hostname)))
34
- return false;
35
- if (url.username || url.password || url.hash)
36
- return false;
37
- return true;
38
- }
39
- catch {
40
- return false;
41
- }
42
- }
43
- /** Validate a registration name. */
44
- export function validateRegistrationName(name) {
45
- const errors = [];
46
- if (typeof name !== 'string') {
47
- errors.push('name must be a string');
48
- }
49
- else if (!isValidRegistrationName(name)) {
50
- errors.push('name must be kebab-case (lowercase letter start, alphanumeric + hyphens)');
51
- }
52
- return { isValid: errors.length === 0, errors };
53
- }
54
- /** Validate an environment variable name. */
55
- export function validateEnvVarName(name) {
56
- const errors = [];
57
- if (typeof name !== 'string') {
58
- errors.push('env var name must be a string');
59
- }
60
- else if (!isValidEnvVarName(name)) {
61
- errors.push('env var name must start with letter/underscore, followed by alphanumeric/underscore');
62
- }
63
- return { isValid: errors.length === 0, errors };
64
- }
65
- /** Validate an endpoint URL. */
66
- export function validateEndpoint(endpoint) {
67
- const errors = [];
68
- if (typeof endpoint !== 'string') {
69
- errors.push('endpoint must be a string');
70
- }
71
- else if (!isValidEndpoint(endpoint)) {
72
- errors.push('endpoint must be an absolute https:// URL (http:// permitted for loopback hosts only; no credentials or fragment)');
73
- }
74
- return { isValid: errors.length === 0, errors };
75
- }
76
- /** Normalize endpoint URL to a canonical form (remove trailing slash if present). */
77
- export function normalizeEndpoint(endpoint) {
78
- try {
79
- const url = new URL(endpoint);
80
- // Reconstruct without trailing slash on path
81
- let pathname = url.pathname;
82
- if (pathname.length > 1 && pathname.endsWith('/')) {
83
- pathname = pathname.slice(0, -1);
84
- }
85
- return `${url.protocol}//${url.host}${pathname}${url.search}`;
86
- }
87
- catch {
88
- return endpoint;
89
- }
90
- }
91
- /** List every EFFECTIVE configured CLI visible across scopes (project nearest-first,
92
- * then user). Dedupes by name: the first-seen registration for a name wins (nearer
93
- * project root, then user) and any same-name registration in a farther scope is
94
- * omitted entirely — there is no shadowed-copy representation and no `effective`
95
- * field on `ConfiguredCliRegistration`. */
96
- export function listConfiguredCliRegistrations() {
97
- const out = [];
98
- const seen = new Set();
99
- // Project scopes, nearest-first (projectScopeRoots is already nearest-first).
100
- // Read EACH root's own config directly and address its own cache dir, so a
101
- // nested/profile-widened stack keeps nearest-project precedence (first-seen
102
- // wins) instead of collapsing every root onto the nearest config. `root` is
103
- // the `configured-clis/` cache parent (matching resolveConfiguredCli and
104
- // configuredCliCacheDir), never the bare scope-root.
105
- for (const scopeRootDir of projectScopeRoots()) {
106
- const config = readConfigAtRoot(scopeRootDir);
107
- const root = configuredClisDirForRoot(scopeRootDir);
108
- for (const [name, entry] of Object.entries(config.configuredClis)) {
109
- if (seen.has(name))
110
- continue;
111
- out.push({
112
- name,
113
- endpoint: entry.endpoint,
114
- authEnvVar: entry.authEnvVar,
115
- scope: 'project',
116
- root,
117
- });
118
- seen.add(name);
119
- }
120
- }
121
- // Then user scope (lowest precedence, so add unseen ones).
122
- const userConfig = readConfig('user');
123
- const userRoot = configuredClisDirForRoot(userScopeRoot());
124
- for (const [name, entry] of Object.entries(userConfig.configuredClis)) {
125
- if (seen.has(name))
126
- continue;
127
- out.push({
128
- name,
129
- endpoint: entry.endpoint,
130
- authEnvVar: entry.authEnvVar,
131
- scope: 'user',
132
- root: userRoot,
133
- });
134
- seen.add(name);
135
- }
136
- return out;
137
- }
138
- /** Get deduplicated effective registrations (one per unique name, project wins over user). */
139
- export function effectiveConfiguredClis() {
140
- return listConfiguredCliRegistrations();
141
- }
142
- /** Resolve a configured CLI registration by name. If scope is omitted, returns the
143
- * effective winner (project > user). If scope is explicit, returns the registration
144
- * in that scope or null if not found. */
145
- export function resolveConfiguredCli(name, scope) {
146
- const all = listConfiguredCliRegistrations();
147
- if (scope === undefined) {
148
- // Return effective winner (first in list after precedence ordering)
149
- return all.find((r) => r.name === name) ?? null;
150
- }
151
- if (scope !== 'project' && scope !== 'user') {
152
- return null; // builtin scope is not applicable
153
- }
154
- if (scope === 'project') {
155
- // Walk every visible project root (nearest-first), matching the same
156
- // precedence `listConfiguredCliRegistrations` uses — an explicit
157
- // `--scope project` must find a registration in a farther project root,
158
- // not only the nearest one.
159
- for (const scopeRootDir of projectScopeRoots()) {
160
- const config = readConfigAtRoot(scopeRootDir);
161
- const entry = config.configuredClis[name];
162
- if (!entry)
163
- continue;
164
- return {
165
- name,
166
- endpoint: entry.endpoint,
167
- authEnvVar: entry.authEnvVar,
168
- scope: 'project',
169
- root: configuredClisDirForRoot(scopeRootDir),
170
- };
171
- }
172
- return null;
173
- }
174
- // user scope: single root.
175
- const config = readConfig('user');
176
- const entry = config.configuredClis[name];
177
- if (!entry)
178
- return null;
179
- const root = configuredClisDir('user');
180
- if (!root)
181
- return null;
182
- return {
183
- name,
184
- endpoint: entry.endpoint,
185
- authEnvVar: entry.authEnvVar,
186
- scope: 'user',
187
- root,
188
- };
189
- }
190
- /** Get the cache directory for a configured CLI registration. */
191
- export function configuredCliCacheDir(registration) {
192
- return join(registration.root, registration.name);
193
- }
194
- /** Get the manifest file path for a configured CLI. */
195
- export function configuredCliManifestPath(registration) {
196
- return join(configuredCliCacheDir(registration), 'manifest.json');
197
- }
198
- /** Get the cache metadata file path for a configured CLI. */
199
- export function configuredCliCacheMetaPath(registration) {
200
- return join(configuredCliCacheDir(registration), 'meta.json');
201
- }