@lanes-sh/link 0.5.4 → 0.6.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 (48) hide show
  1. package/instructions/agents/lanes-link-scout.md +1 -1
  2. package/instructions/skills/lanes-link/SKILL.md +33 -29
  3. package/package.json +2 -1
  4. package/src/cli/commands/connect/target-note.ts +2 -2
  5. package/src/cli/commands/knowledge/index.ts +14 -34
  6. package/src/cli/commands/mcp/register.ts +1 -1
  7. package/src/cli/commands/operate/dashboard.ts +2 -2
  8. package/src/cli/commands/operate/inspect.ts +5 -1
  9. package/src/cli/commands/operate/migrate.ts +85 -1
  10. package/src/cli/commands/operate/outputs.ts +7 -22
  11. package/src/cli/commands/operate/status.ts +84 -69
  12. package/src/cli/commands/operate/tools.ts +1 -1
  13. package/src/cli/commands/profile/removal.ts +36 -25
  14. package/src/cli/commands/profile/remove.ts +5 -2
  15. package/src/cli/commands/profile.ts +56 -40
  16. package/src/cli/commands/sync.ts +94 -162
  17. package/src/cli/commands/target.ts +115 -74
  18. package/src/cli/commands/update.ts +56 -1
  19. package/src/cli/config-edit.ts +47 -16
  20. package/src/cli/endpoint-url.ts +3 -3
  21. package/src/cli/main.ts +9 -7
  22. package/src/cli/publish.ts +4 -2
  23. package/src/cli/runtime/open.ts +19 -10
  24. package/src/cli/runtime/select.ts +69 -22
  25. package/src/cli/selection-require.ts +79 -0
  26. package/src/cli/selection.ts +44 -92
  27. package/src/cli/workspace-migrate.ts +385 -0
  28. package/src/deployments/bootstrap.ts +31 -11
  29. package/src/deployments/deploy.ts +54 -27
  30. package/src/deployments/knowledge.ts +5 -2
  31. package/src/deployments/prepare.ts +3 -1
  32. package/src/deployments/serving.ts +19 -15
  33. package/src/deployments/upload.ts +10 -1
  34. package/src/profile/deployments.ts +64 -53
  35. package/src/profile/index.ts +22 -6
  36. package/src/profile/legacy.ts +92 -0
  37. package/src/profile/load.ts +12 -28
  38. package/src/profile/registry.ts +182 -0
  39. package/src/profile/schema.ts +162 -110
  40. package/src/profile/targets.ts +62 -91
  41. package/src/profile/testing.ts +78 -0
  42. package/src/profile/workspace.ts +11 -25
  43. package/src/server/dashboard.ts +4 -1
  44. package/src/server/harness.ts +1 -6
  45. package/src/cli/commands/profile/declare.ts +0 -154
  46. package/src/deployments/servable.ts +0 -82
  47. package/src/deployments/sync-apply.ts +0 -330
  48. package/src/deployments/sync.ts +0 -164
@@ -1,164 +0,0 @@
1
- import type { Config } from '#profile';
2
-
3
- /**
4
- * What differs between a workspace and a target's copy of it.
5
- *
6
- * A deployed endpoint reads its config out of a bucket, and `deploy` puts it
7
- * there — so from the moment of the first deploy there are two copies of every
8
- * profile. They are supposed to agree. When they do not, one of them holds
9
- * something the other has lost, and until now nothing could say which or bring
10
- * it back.
11
- *
12
- * This file only *finds* the difference. Deciding it and writing it are
13
- * `sync-apply.ts`, and choosing which bucket to compare against is the command.
14
- * Split that way because the diff is the part worth testing exhaustively and
15
- * the only part with no I/O in it.
16
- */
17
-
18
- /** Which side is missing what, or that both have it and disagree. */
19
- export type Direction = 'pull' | 'push' | 'conflict';
20
-
21
- export interface Change {
22
- /** Where in the config, e.g. `['targets', 'cloud']` or `['connections']`. */
23
- readonly path: readonly string[];
24
- readonly direction: Direction;
25
- /** What remote holds. Absent when remote is the side that is missing it. */
26
- readonly remote?: unknown;
27
- /** What local holds. Absent when local is the side that is missing it. */
28
- readonly local?: unknown;
29
- }
30
-
31
- /**
32
- * Arrays whose elements are records rather than an ordered list.
33
- *
34
- * `connections` is a set of accounts keyed by `provider.id`; comparing it
35
- * positionally would call a reordered file a conflict and, worse, would call an
36
- * *added* connection a change to whichever one now sits at that index. The key
37
- * function is what makes "personal gained a mailbox" a different fact from
38
- * "personal's third connection changed".
39
- *
40
- * `identity` is deliberately absent. Its declaration order is meaningful — the
41
- * first entry of a kind is the one to reach for — so it is an ordered list and
42
- * compares as one.
43
- */
44
- const KEYED: Record<string, (item: unknown) => string | undefined> = {
45
- connections: (item) =>
46
- isRecord(item) ? `${String(item['provider'])}.${String(item['id'])}` : undefined,
47
- // Two shapes, and both are reached. The diff runs over validated configs,
48
- // where `allow: [gmail.*]` has become `[{capability: gmail.*}]`; the writer
49
- // runs over the raw document, where it is still a string. A key function that
50
- // knew only the validated shape found nothing to merge and silently wrote the
51
- // array without it.
52
- 'policy.allow': capabilityOf,
53
- 'policy.deny': capabilityOf,
54
- };
55
-
56
- function capabilityOf(item: unknown): string | undefined {
57
- if (typeof item === 'string') return item;
58
- return isRecord(item) ? String(item['capability']) : undefined;
59
- }
60
-
61
- const isRecord = (value: unknown): value is Record<string, unknown> =>
62
- typeof value === 'object' && value !== null && !Array.isArray(value);
63
-
64
- const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
65
-
66
- /**
67
- * Every difference between one profile's two copies.
68
- *
69
- * Recursive over objects so a change reports at the narrowest path that
70
- * describes it: two profiles differing only in `auth.authorization` produce
71
- * that path, not `auth`, and applying it cannot take `token_ref` with it.
72
- */
73
- export function diffConfigs(local: Config | undefined, remote: Config | undefined): Change[] {
74
- if (local === undefined && remote === undefined) return [];
75
-
76
- // A whole profile that only one side has. Reported at the root, because the
77
- // unit that has to be copied is the file.
78
- if (local === undefined) return [{ path: [], direction: 'pull', remote }];
79
- if (remote === undefined) return [{ path: [], direction: 'push', local }];
80
-
81
- return walk([], local as unknown, remote as unknown);
82
- }
83
-
84
- function walk(path: readonly string[], local: unknown, remote: unknown): Change[] {
85
- if (same(local, remote)) return [];
86
-
87
- if (local === undefined) return [{ path, direction: 'pull', remote }];
88
- if (remote === undefined) return [{ path, direction: 'push', local }];
89
-
90
- const key = path.join('.');
91
- const keyOf = KEYED[key];
92
- if (keyOf && Array.isArray(local) && Array.isArray(remote)) {
93
- return walkKeyed(path, keyOf, local, remote);
94
- }
95
-
96
- if (isRecord(local) && isRecord(remote)) {
97
- const keys = [...new Set([...Object.keys(local), ...Object.keys(remote)])].sort();
98
- return keys.flatMap((name) => walk([...path, name], local[name], remote[name]));
99
- }
100
-
101
- // Two scalars, or two ordered arrays, that are not equal. There is no
102
- // narrower path to report and no way to have both.
103
- return [{ path, direction: 'conflict', local, remote }];
104
- }
105
-
106
- /**
107
- * A keyed array, compared as the set of records it is.
108
- *
109
- * Each element reports under `path.key` — `connections.gmail.work` — so a
110
- * missing account names itself in the output instead of appearing as an index.
111
- * Applying still rewrites the whole array, because a YAML sequence has no
112
- * addressable slot for "the element whose provider is gmail".
113
- */
114
- function walkKeyed(
115
- path: readonly string[],
116
- keyOf: (item: unknown) => string | undefined,
117
- local: readonly unknown[],
118
- remote: readonly unknown[],
119
- ): Change[] {
120
- const index = (items: readonly unknown[]): Map<string, unknown> =>
121
- new Map(
122
- items
123
- .map((item) => [keyOf(item), item] as const)
124
- .filter((pair): pair is readonly [string, unknown] => pair[0] !== undefined),
125
- );
126
-
127
- const here = index(local);
128
- const there = index(remote);
129
- const names = [...new Set([...here.keys(), ...there.keys()])].sort();
130
-
131
- return names.flatMap((name) => walk([...path, name], here.get(name), there.get(name)));
132
- }
133
-
134
- /**
135
- * The array a change belongs to, when it belongs to one.
136
- *
137
- * `['connections', 'gmail.work']` is applied by rewriting `connections`, so
138
- * both the writer and the renderer need to know where the element stops and the
139
- * key begins. Longest prefix first, so `policy.allow` wins over `policy`.
140
- */
141
- export function keyedArrayFor(path: readonly string[]): readonly string[] | undefined {
142
- for (let depth = path.length - 1; depth > 0; depth--) {
143
- if (path.slice(0, depth).join('.') in KEYED) return path.slice(0, depth);
144
- }
145
- return undefined;
146
- }
147
-
148
- /**
149
- * How an element of a keyed array identifies itself, for the writer.
150
- *
151
- * The diff indexes these to compare them; applying one has to find the same
152
- * element again in *both* raw documents, and it cannot re-derive the key from
153
- * the path — `connections.gmail.work` is one key containing a dot, not two
154
- * steps. Exported so the two halves cannot disagree about what identifies a
155
- * connection.
156
- */
157
- export function keyOfElement(arrayPath: readonly string[], item: unknown): string | undefined {
158
- return KEYED[arrayPath.join('.')]?.(item);
159
- }
160
-
161
- /** Whether a set of changes can be applied without being told which side wins. */
162
- export function conflictsIn(changes: readonly Change[]): Change[] {
163
- return changes.filter((change) => change.direction === 'conflict');
164
- }