@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.
- package/instructions/agents/lanes-link-scout.md +1 -1
- package/instructions/skills/lanes-link/SKILL.md +33 -29
- package/package.json +2 -1
- package/src/cli/commands/connect/target-note.ts +2 -2
- package/src/cli/commands/knowledge/index.ts +14 -34
- package/src/cli/commands/mcp/register.ts +1 -1
- package/src/cli/commands/operate/dashboard.ts +2 -2
- package/src/cli/commands/operate/inspect.ts +5 -1
- package/src/cli/commands/operate/migrate.ts +85 -1
- package/src/cli/commands/operate/outputs.ts +7 -22
- package/src/cli/commands/operate/status.ts +84 -69
- package/src/cli/commands/operate/tools.ts +1 -1
- package/src/cli/commands/profile/removal.ts +36 -25
- package/src/cli/commands/profile/remove.ts +5 -2
- package/src/cli/commands/profile.ts +56 -40
- package/src/cli/commands/sync.ts +94 -162
- package/src/cli/commands/target.ts +115 -74
- package/src/cli/commands/update.ts +56 -1
- package/src/cli/config-edit.ts +47 -16
- package/src/cli/endpoint-url.ts +3 -3
- package/src/cli/main.ts +9 -7
- package/src/cli/publish.ts +4 -2
- package/src/cli/runtime/open.ts +19 -10
- package/src/cli/runtime/select.ts +69 -22
- package/src/cli/selection-require.ts +79 -0
- package/src/cli/selection.ts +44 -92
- package/src/cli/workspace-migrate.ts +385 -0
- package/src/deployments/bootstrap.ts +31 -11
- package/src/deployments/deploy.ts +54 -27
- package/src/deployments/knowledge.ts +5 -2
- package/src/deployments/prepare.ts +3 -1
- package/src/deployments/serving.ts +19 -15
- package/src/deployments/upload.ts +10 -1
- package/src/profile/deployments.ts +64 -53
- package/src/profile/index.ts +22 -6
- package/src/profile/legacy.ts +92 -0
- package/src/profile/load.ts +12 -28
- package/src/profile/registry.ts +182 -0
- package/src/profile/schema.ts +162 -110
- package/src/profile/targets.ts +62 -91
- package/src/profile/testing.ts +78 -0
- package/src/profile/workspace.ts +11 -25
- package/src/server/dashboard.ts +4 -1
- package/src/server/harness.ts +1 -6
- package/src/cli/commands/profile/declare.ts +0 -154
- package/src/deployments/servable.ts +0 -82
- package/src/deployments/sync-apply.ts +0 -330
- package/src/deployments/sync.ts +0 -164
package/src/deployments/sync.ts
DELETED
|
@@ -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
|
-
}
|