@lanes-sh/link 0.5.1 → 0.5.3
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/package.json
CHANGED
|
@@ -138,6 +138,57 @@ export function connectionsSharingCredential(
|
|
|
138
138
|
.map((one) => `${one.provider}.${one.id}`);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Take back the allow rules that named the provider, once nothing declares it.
|
|
143
|
+
*
|
|
144
|
+
* Not a tidy-up. `assertReferentialIntegrity` refuses an allow rule naming a
|
|
145
|
+
* provider with no connection, and `save` validates — so disconnecting the last
|
|
146
|
+
* connection of a provider *failed*, with a config error about a policy line the
|
|
147
|
+
* operator had not touched, and nothing removed. Every single-account provider
|
|
148
|
+
* was undisconnectable, which is most of them: the bug reproduced on `slack.*`
|
|
149
|
+
* and would have on `bunq.*`, while a profile with two Gmail accounts could
|
|
150
|
+
* disconnect one perfectly well.
|
|
151
|
+
*
|
|
152
|
+
* Symmetry is the argument for removing rather than warning. `connect` writes
|
|
153
|
+
* the row *and* the rule, and the pair is what `config-repair.ts` calls both
|
|
154
|
+
* halves or neither — a rule with nothing behind it grants nothing and is what
|
|
155
|
+
* that file exists to stop being written.
|
|
156
|
+
*
|
|
157
|
+
* `allow: ['*']` is untouched: it names no provider, so nothing about it becomes
|
|
158
|
+
* false. Narrower rules go with the wide one — `gmail.send_message` is as
|
|
159
|
+
* dangling as `gmail.*` once the last Gmail is gone, and the loader refuses it
|
|
160
|
+
* for the same reason.
|
|
161
|
+
*
|
|
162
|
+
* `deny` is left alone, deliberately. The loader permits a deny naming a
|
|
163
|
+
* provider with no connection, because denying something you have not connected
|
|
164
|
+
* yet is a reasonable thing to write ahead of time — and removing it here would
|
|
165
|
+
* silently re-permit whatever it covered if the account came back.
|
|
166
|
+
*/
|
|
167
|
+
function dropProviderRules(document: ConfigDocument, config: Config, index: number): void {
|
|
168
|
+
const going = config.connections[index];
|
|
169
|
+
if (!going) return;
|
|
170
|
+
|
|
171
|
+
const stillDeclared = config.connections.some(
|
|
172
|
+
(one, i) => i !== index && one.provider === going.provider,
|
|
173
|
+
);
|
|
174
|
+
if (stillDeclared) return;
|
|
175
|
+
|
|
176
|
+
const rules = document.getIn(['policy', 'allow']) as { items?: unknown[] } | null;
|
|
177
|
+
const doomed: number[] = [];
|
|
178
|
+
|
|
179
|
+
(rules?.items ?? []).forEach((_rule, at) => {
|
|
180
|
+
const bare = document.getIn(['policy', 'allow', at]);
|
|
181
|
+
const capability =
|
|
182
|
+
typeof bare === 'string' ? bare : document.getIn(['policy', 'allow', at, 'capability']);
|
|
183
|
+
if (typeof capability !== 'string') return;
|
|
184
|
+
|
|
185
|
+
if (capability.split('.')[0] === going.provider) doomed.push(at);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Descending, so each removal cannot move the index of one still to come.
|
|
189
|
+
for (const at of doomed.reverse()) document.removeFrom(['policy', 'allow'], at);
|
|
190
|
+
}
|
|
191
|
+
|
|
141
192
|
export async function removeConnection(
|
|
142
193
|
key: string,
|
|
143
194
|
flags: DisconnectFlags,
|
|
@@ -162,6 +213,10 @@ export async function removeConnection(
|
|
|
162
213
|
// running `connect`. The reverse — credential gone, declaration kept, edit
|
|
163
214
|
// failed — is the same state, so ordering costs nothing either way; doing the
|
|
164
215
|
// edit first means the file is right even if the store is unreachable.
|
|
216
|
+
// Both edits before the one `save`, so the file is never written in the
|
|
217
|
+
// state where the row is gone and the rule that named it is not — which is
|
|
218
|
+
// the state the loader refuses.
|
|
219
|
+
dropProviderRules(document, config, located.index);
|
|
165
220
|
document.removeFrom(['connections'], located.index);
|
|
166
221
|
await document.save();
|
|
167
222
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join, sep } from 'node:path';
|
|
3
|
-
import { installRoot } from '#profile';
|
|
3
|
+
import { installRoot, resolveWorkspaceRoot } from '#profile';
|
|
4
|
+
import { repairOwnerLayer } from '../config-repair.ts';
|
|
4
5
|
import { emit, fail, ok, print, printErr, progress, style, warn } from '../output.ts';
|
|
5
6
|
import { PACKAGE, release, type ReleaseState } from '../release.ts';
|
|
6
7
|
import { version } from '../version.ts';
|
|
@@ -152,6 +153,27 @@ export async function update(flags: UpdateFlags): Promise<void> {
|
|
|
152
153
|
// was down would make this the flakiest check in it.
|
|
153
154
|
if (flags.check === true && decision.action === 'install') process.exitCode = 1;
|
|
154
155
|
|
|
156
|
+
// Whatever the registry said, and before the branch that returns early.
|
|
157
|
+
//
|
|
158
|
+
// `start`, `connect` and `deploy` already repair a profile that is missing
|
|
159
|
+
// part of the owner layer, and for months that was enough. It is not: a
|
|
160
|
+
// release that adds a surface — `tasks` and `assets` in 0.5.0 — leaves every
|
|
161
|
+
// existing profile without it until one of those three next runs, and someone
|
|
162
|
+
// who serves their endpoint from elsewhere may run none of them for weeks. The
|
|
163
|
+
// page they look at meanwhile offers to *add* what they already have,
|
|
164
|
+
// which is where this was reported from.
|
|
165
|
+
//
|
|
166
|
+
// `update` is the command that means "bring me current", so it is the honest
|
|
167
|
+
// place for the other half of current. Not on `--check`, which is a question
|
|
168
|
+
// and must not write, and not conditional on an install having happened: the
|
|
169
|
+
// profile of someone already on the latest version is exactly the one this was
|
|
170
|
+
// reported against.
|
|
171
|
+
if (flags.check !== true) {
|
|
172
|
+
await repairOwnerLayer(resolveWorkspaceRoot(), undefined, {
|
|
173
|
+
...(flags.json === true ? { report: progress } : {}),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
155
177
|
const report = {
|
|
156
178
|
installed: current.installed,
|
|
157
179
|
latest: current.latest,
|
package/src/cli/config-repair.ts
CHANGED
|
@@ -261,7 +261,14 @@ export function ensureIdentityConnection(document: ConfigDocument): SurfaceRepai
|
|
|
261
261
|
export async function repairOwnerLayer(
|
|
262
262
|
workspaceRoot: string,
|
|
263
263
|
profiles: readonly string[] | undefined,
|
|
264
|
+
options: { report?: (line: string) => void } = {},
|
|
264
265
|
): Promise<void> {
|
|
266
|
+
// stdout by default, because every caller but one is printing a report a
|
|
267
|
+
// person reads. `update --json` passes `progress` instead: what it produces is
|
|
268
|
+
// a document, and a line of prose in front of it corrupts whatever is parsing.
|
|
269
|
+
// Routed rather than silenced — nothing else here widens a policy without
|
|
270
|
+
// saying so, and this must not be the exception.
|
|
271
|
+
const say = options.report ?? print;
|
|
265
272
|
const wanted = profiles === undefined ? undefined : new Set(profiles);
|
|
266
273
|
|
|
267
274
|
for (const name of await listProfiles(workspaceRoot)) {
|
|
@@ -274,13 +281,13 @@ export async function repairOwnerLayer(
|
|
|
274
281
|
|
|
275
282
|
await document.save();
|
|
276
283
|
|
|
277
|
-
|
|
278
|
-
for (const change of repairLines(repair))
|
|
279
|
-
|
|
284
|
+
say(ok(`gave ${style.bold(name)} its own owner layer`));
|
|
285
|
+
for (const change of repairLines(repair)) say(` ${style.dim(change)}`);
|
|
286
|
+
say(
|
|
280
287
|
` ${style.dim('memory, tasks, assets, skills, vault and setup — your own material, no account behind any of them')}`,
|
|
281
288
|
);
|
|
282
289
|
} catch (error) {
|
|
283
|
-
|
|
290
|
+
say(
|
|
284
291
|
warn(
|
|
285
292
|
`could not give ${name} its owner layer: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`,
|
|
286
293
|
),
|