@lanes-sh/link 0.7.2 → 0.8.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/README.md +20 -10
- package/instructions/agents/lanes-link-scout.md +2 -2
- package/instructions/skills/lanes-link/SKILL.md +136 -61
- package/package.json +2 -1
- package/src/audit/index.ts +8 -1
- package/src/auth/index.ts +58 -2
- package/src/auth/lanes/assertion.ts +256 -0
- package/src/auth/lanes/callback.ts +135 -0
- package/src/auth/lanes/federation.ts +50 -0
- package/src/auth/lanes/login.ts +294 -0
- package/src/auth/lanes/members.ts +103 -0
- package/src/auth/lanes/session.ts +97 -0
- package/src/auth/oauth/grant.ts +183 -0
- package/src/auth/oauth/result.ts +27 -0
- package/src/auth/oauth/server.ts +176 -203
- package/src/auth/oauth/store.ts +65 -0
- package/src/auth/remote.ts +32 -9
- package/src/cli/accepts.ts +108 -0
- package/src/cli/argv.ts +57 -3
- package/src/cli/audit-change.ts +140 -0
- package/src/cli/callback-page.ts +36 -115
- package/src/cli/commands/auth-dispatch.ts +48 -0
- package/src/cli/commands/auth.ts +229 -0
- package/src/cli/commands/connect/accounts.ts +4 -4
- package/src/cli/commands/connect/authorise.ts +4 -4
- package/src/cli/commands/connect/bind-credential.ts +2 -1
- package/src/cli/commands/connect/custom/index.ts +1 -1
- package/src/cli/commands/connect/custom/write.ts +2 -2
- package/src/cli/commands/connect/grant.ts +29 -14
- package/src/cli/commands/connect/index.ts +88 -87
- package/src/cli/commands/connect/options.ts +83 -0
- package/src/cli/commands/connect/registration.ts +50 -0
- package/src/cli/commands/connect/requirements.ts +1 -1
- package/src/cli/commands/connect/settle.ts +4 -2
- package/src/cli/commands/connect/target-note.ts +7 -2
- package/src/cli/commands/connect/unknown.ts +1 -1
- package/src/cli/commands/connect/variables.ts +3 -2
- package/src/cli/commands/connection-list.ts +116 -0
- package/src/cli/commands/connection.ts +182 -165
- package/src/cli/commands/grant.ts +140 -0
- package/src/cli/commands/identity.ts +21 -9
- package/src/cli/commands/knowledge/index.ts +46 -79
- package/src/cli/commands/knowledge/migrate.ts +74 -13
- package/src/cli/commands/knowledge/show.ts +92 -0
- package/src/cli/commands/knowledge.ts +2 -1
- package/src/cli/commands/mcp/harnesses.ts +30 -8
- package/src/cli/commands/mcp/onboarding.ts +86 -0
- package/src/cli/commands/mcp/register.ts +16 -2
- package/src/cli/commands/mcp.ts +1 -0
- package/src/cli/commands/members.ts +288 -0
- package/src/cli/commands/operate/attach.ts +3 -3
- package/src/cli/commands/operate/audit.ts +11 -7
- package/src/cli/commands/operate/auth.ts +28 -11
- package/src/cli/commands/operate/findings.ts +2 -1
- package/src/cli/commands/operate/inspect.ts +37 -19
- package/src/cli/commands/operate/migrate.ts +29 -12
- package/src/cli/commands/operate/outputs.ts +3 -3
- package/src/cli/commands/operate/pair-certificate.ts +141 -0
- package/src/cli/commands/operate/pair.ts +324 -0
- package/src/cli/commands/operate/policy.ts +73 -22
- package/src/cli/commands/operate/serve.ts +52 -4
- package/src/cli/commands/operate/status.ts +18 -10
- package/src/cli/commands/operate/tools.ts +2 -2
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/owner/shared.ts +13 -2
- package/src/cli/commands/owner/skills.ts +28 -8
- package/src/cli/commands/profile/removal.ts +79 -76
- package/src/cli/commands/profile/remove.ts +16 -1
- package/src/cli/commands/profile.ts +46 -10
- package/src/cli/commands/relabel.ts +112 -0
- package/src/cli/commands/secrets.ts +34 -12
- package/src/cli/commands/set-workspace.ts +96 -0
- package/src/cli/commands/setup.ts +2 -2
- package/src/cli/commands/sync.ts +8 -8
- package/src/cli/commands/target.ts +9 -7
- package/src/cli/commands/update.ts +58 -17
- package/src/cli/config-edit.ts +75 -140
- package/src/cli/config-migrate.ts +82 -64
- package/src/cli/config-repair.ts +89 -32
- package/src/cli/config-templates.ts +198 -0
- package/src/cli/contract3-data.ts +328 -0
- package/src/cli/contract3-shape.ts +186 -0
- package/src/cli/contract3.ts +282 -0
- package/src/cli/endpoint-url.ts +1 -1
- package/src/cli/lanes.ts +25 -1
- package/src/cli/main.ts +89 -14
- package/src/cli/migrate-plan.ts +12 -6
- package/src/cli/output.ts +34 -1
- package/src/cli/publish.ts +5 -2
- package/src/cli/runtime/open.ts +63 -98
- package/src/cli/runtime/registry.ts +6 -7
- package/src/cli/runtime/stores.ts +53 -0
- package/src/cli/runtime/types.ts +106 -0
- package/src/cli/runtime/vault.ts +19 -4
- package/src/cli/runtime/workspace.ts +60 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection-require.ts +44 -13
- package/src/cli/selection.ts +127 -145
- package/src/cli/usage.ts +34 -18
- package/src/cli/workspace-migrate.ts +125 -16
- package/src/connectivity/manifest/provider.ts +3 -1
- package/src/connectivity/manifest/requirements.ts +1 -1
- package/src/deployments/bind.ts +1 -1
- package/src/deployments/deploy.ts +36 -27
- package/src/deployments/gcp/bucket.ts +18 -7
- package/src/deployments/gcp/provision.ts +7 -7
- package/src/deployments/prepare.ts +72 -24
- package/src/deployments/record.ts +1 -1
- package/src/deployments/report.ts +2 -2
- package/src/deployments/serving.ts +15 -74
- package/src/deployments/target.ts +15 -15
- package/src/deployments/upload.ts +46 -22
- package/src/dispatch/deps.ts +88 -0
- package/src/dispatch/dispatch.ts +21 -62
- package/src/policy/index.ts +47 -15
- package/src/profile/connections.ts +183 -0
- package/src/profile/deployments.ts +3 -3
- package/src/profile/index.ts +30 -5
- package/src/profile/layout.ts +86 -89
- package/src/profile/load.ts +80 -47
- package/src/profile/pairing.ts +32 -0
- package/src/profile/primitives.ts +35 -1
- package/src/profile/registry.ts +6 -6
- package/src/profile/schema.ts +172 -21
- package/src/profile/targets.ts +21 -9
- package/src/profile/testing.ts +69 -2
- package/src/profile/workspace.ts +58 -3
- package/src/providers/custom/index.ts +1 -1
- package/src/providers/custom/load.ts +2 -3
- package/src/providers/identity/provider.ts +1 -1
- package/src/providers/memory/provider.ts +20 -2
- package/src/providers/setup/plan.ts +1 -1
- package/src/providers/slack/index.ts +2 -2
- package/src/registry/policy-bridge.ts +33 -11
- package/src/registry/reconcile.ts +4 -4
- package/src/server/authorization.ts +94 -0
- package/src/server/edge.ts +14 -1
- package/src/server/endpoint.ts +85 -104
- package/src/server/generation.ts +10 -1
- package/src/server/harness.ts +71 -13
- package/src/server/index.ts +31 -0
- package/src/server/mcp/build.ts +20 -1
- package/src/server/mcp/client-info.ts +54 -0
- package/src/server/mcp/guide.ts +120 -0
- package/src/server/mcp/instructions.ts +1 -1
- package/src/server/mcp/prompts.ts +7 -3
- package/src/server/mcp/resources.ts +16 -8
- package/src/server/mcp/tools.ts +9 -3
- package/src/server/mcp/visibility.ts +18 -3
- package/src/server/oauth.ts +29 -109
- package/src/server/read/credential.ts +134 -0
- package/src/server/read/deployed.ts +56 -0
- package/src/server/read/listener.ts +54 -0
- package/src/server/read/open.ts +101 -0
- package/src/server/read/routes.ts +247 -0
- package/src/server/read/state.ts +171 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { layout } from '#profile';
|
|
2
|
+
import type { BlobStore } from '#stores/blobs';
|
|
3
|
+
import type { StorageFactory } from '#deployments/target.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where the owner layer's bytes are, for a runtime being assembled.
|
|
7
|
+
*
|
|
8
|
+
* Split from `open.ts` so that file stays inside the size budget, on the seam
|
|
9
|
+
* it already had: `open.ts` decides what a runtime *is* and this decides where
|
|
10
|
+
* one store lives. Both roots are explicit areas rather than derived from
|
|
11
|
+
* `storage.path`, matching `openState` and `openAudit` — a workspace that moves
|
|
12
|
+
* its provider blobs does not move the reserved roots beside them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A store with nothing in it, for a profile that grants no skills connection.
|
|
17
|
+
*
|
|
18
|
+
* Written out rather than imported from `#stores/blobs/testing.ts`: that module
|
|
19
|
+
* exists for tests, and production code reaching into it is how a test double
|
|
20
|
+
* ends up on a real path. Five methods, none of which can fail, is cheaper than
|
|
21
|
+
* the import would be to justify.
|
|
22
|
+
*/
|
|
23
|
+
export const EMPTY_SKILL_STORE: BlobStore = {
|
|
24
|
+
get: async () => null,
|
|
25
|
+
put: async () => {
|
|
26
|
+
throw new Error('this profile grants no skills connection');
|
|
27
|
+
},
|
|
28
|
+
has: async () => false,
|
|
29
|
+
delete: async () => {},
|
|
30
|
+
list: async () => [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Where a connection's skills live, in either workspace.
|
|
35
|
+
*
|
|
36
|
+
* `data/skills.d/<connection>/`, and deployed the same key under the bucket
|
|
37
|
+
* prefix. Going through the store rather than a filesystem path is what gives a
|
|
38
|
+
* deployment skills at all — a path is baked into a container image at build
|
|
39
|
+
* time and an object key is not, so before ADR-014 a deployed instance could
|
|
40
|
+
* only ever serve the skills that existed when its image was built.
|
|
41
|
+
*
|
|
42
|
+
* **Per connection**, which is the third answer this question has had. Policy
|
|
43
|
+
* gating `skills.<name>` was the whole isolation story while the bytes were
|
|
44
|
+
* shared (ADR-012 §1), and it is a weak one: it decides who may *run* a
|
|
45
|
+
* procedure, not who may read that it exists or what it says. ADR-030 made the
|
|
46
|
+
* bytes per profile; ADR-059 made them per connection, so two profiles granting
|
|
47
|
+
* one skills connection share a set and two granting different ones share
|
|
48
|
+
* nothing.
|
|
49
|
+
*/
|
|
50
|
+
export function skillStore(storage: StorageFactory, connection: string): BlobStore {
|
|
51
|
+
return storage(layout.skills(connection));
|
|
52
|
+
}
|
|
53
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { BearerAuthenticator } from '#auth';
|
|
2
|
+
import type { SecretStore } from '#secrets';
|
|
3
|
+
import type { AuditReader } from '#audit';
|
|
4
|
+
import type { RuntimeState } from '#stores/state';
|
|
5
|
+
import type { BlobStore } from '#stores/blobs';
|
|
6
|
+
import type { AnyConnector, ProviderManifest } from '#connectivity';
|
|
7
|
+
import type {
|
|
8
|
+
Config,
|
|
9
|
+
ConnectionConfig,
|
|
10
|
+
Resolution,
|
|
11
|
+
SelectedConnection,
|
|
12
|
+
TargetConfig,
|
|
13
|
+
} from '#profile';
|
|
14
|
+
import type { ProviderRegistry } from '#registry';
|
|
15
|
+
import type { Dispatcher } from '#dispatch';
|
|
16
|
+
import type { VaultStore } from '#providers/owner.ts';
|
|
17
|
+
import type { KnowledgeStores } from '#deployments/knowledge.ts';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a command is handed once a profile has been opened.
|
|
21
|
+
*
|
|
22
|
+
* Its own file so `open.ts` stays inside the size budget, and because this is
|
|
23
|
+
* the thing every command in the CLI is written against — a reader looking for
|
|
24
|
+
* "what do I get" should not have to read the assembly to find it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface Runtime {
|
|
28
|
+
readonly resolution: Resolution;
|
|
29
|
+
readonly config: Config;
|
|
30
|
+
readonly target: string;
|
|
31
|
+
/**
|
|
32
|
+
* The target's adapter set, from the workspace that declares it.
|
|
33
|
+
*
|
|
34
|
+
* Here because the config no longer carries it. Every caller that used to
|
|
35
|
+
* reach `config.targets[target]` — for a `deploy` block, a storage adapter, a
|
|
36
|
+
* knowledge repository — reads this instead, and reads it without following
|
|
37
|
+
* the pointer a second time (ADR-052).
|
|
38
|
+
*/
|
|
39
|
+
readonly declared: TargetConfig;
|
|
40
|
+
/**
|
|
41
|
+
* The accounts this profile selects, joined to the grants that govern them.
|
|
42
|
+
*
|
|
43
|
+
* The join is done once, here, because everything that asks — status, doctor,
|
|
44
|
+
* reconcile, setup — wants the same answer and three implementations of it is
|
|
45
|
+
* three chances to disagree (`profile/connections.ts`).
|
|
46
|
+
*/
|
|
47
|
+
readonly connections: readonly SelectedConnection[];
|
|
48
|
+
/** Every account the workspace holds, granted or not. `connection list` reads this. */
|
|
49
|
+
readonly workspaceConnections: readonly ConnectionConfig[];
|
|
50
|
+
/** Vendors this workspace registered a client of its own for (`oauth_apps`). */
|
|
51
|
+
readonly ownClients: readonly string[];
|
|
52
|
+
readonly state: RuntimeState;
|
|
53
|
+
/** The durable log, for reading. Copies, if any, are write-only and not here. */
|
|
54
|
+
readonly audit: AuditReader;
|
|
55
|
+
readonly credentials: SecretStore;
|
|
56
|
+
readonly storage: BlobStore;
|
|
57
|
+
/**
|
|
58
|
+
* Where this profile's skills are kept — `data/skills.d/<connection>/` locally.
|
|
59
|
+
*
|
|
60
|
+
* `undefined` when the profile grants no `skills` connection. That is a real
|
|
61
|
+
* state rather than an empty one, and it is worth the optionality: the owner
|
|
62
|
+
* layer arrives granted (ADR-050), so a profile without it was denied it on
|
|
63
|
+
* purpose, and handing back an empty store would report "no skills" for a
|
|
64
|
+
* profile that is not allowed to have any — the same answer for two different
|
|
65
|
+
* facts.
|
|
66
|
+
*/
|
|
67
|
+
readonly skills: BlobStore | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Present only when this target keeps memory and skills in a repository.
|
|
70
|
+
*
|
|
71
|
+
* Nothing on the dispatch path reads it: `storage` and `skills` above are
|
|
72
|
+
* already pointed at the right bytes, which is the whole design. It is here
|
|
73
|
+
* so `target show` and `doctor` can say where those bytes are without
|
|
74
|
+
* re-reading the config, and so the migration can reach the repository it is
|
|
75
|
+
* committing to.
|
|
76
|
+
*/
|
|
77
|
+
readonly knowledge?: KnowledgeStores | undefined;
|
|
78
|
+
/** The vault's own store, so `lanes link vault` reaches the same bytes MCP does. */
|
|
79
|
+
readonly vault: VaultStore;
|
|
80
|
+
readonly registry: ProviderRegistry;
|
|
81
|
+
/**
|
|
82
|
+
* Re-read the skills into the registry when they have changed on the store.
|
|
83
|
+
*
|
|
84
|
+
* Cheap and idempotent — one `list()`, and a rebuild only when the listing
|
|
85
|
+
* differs from the last one. The caller decides how often to ask.
|
|
86
|
+
*/
|
|
87
|
+
refreshSkills(): Promise<void>;
|
|
88
|
+
readonly dispatcher: Dispatcher;
|
|
89
|
+
readonly authenticator: BearerAuthenticator;
|
|
90
|
+
/** Same factory the dispatcher uses, exposed for commands that probe upstream. */
|
|
91
|
+
connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Same authorizer the dispatcher uses, for the same reason `connectorFor` is here.
|
|
94
|
+
*
|
|
95
|
+
* A command that probes upstream needs the credential on the request, and it
|
|
96
|
+
* must not learn how to put it there — that is one switch on the resolved
|
|
97
|
+
* shape (`connectivity/auth/authorize.ts`) and a second copy would be a
|
|
98
|
+
* second answer. `settleIdentity` is the caller: it asks a provider whose
|
|
99
|
+
* account was just authorised, over whatever method that provider declares.
|
|
100
|
+
*/
|
|
101
|
+
authorizeRequest(providerId: string, connectionId: string, request: Request): Promise<Request>;
|
|
102
|
+
/** A provider's manifest, so an omitted `credential_ref` can be derived from it. */
|
|
103
|
+
manifestFor(providerId: string): ProviderManifest | undefined;
|
|
104
|
+
close(): Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
|
package/src/cli/runtime/vault.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
soleGrantFor, layout, vaultRef, workspacePath } from '#profile';
|
|
2
3
|
import type { SecretStore } from '#secrets';
|
|
3
4
|
import {
|
|
4
5
|
createBlobVaultStore,
|
|
@@ -36,10 +37,15 @@ export function openVault(
|
|
|
36
37
|
const { declared, config, root } = input;
|
|
37
38
|
const vault = declared.vault ?? { adapter: 'file' as const };
|
|
38
39
|
|
|
40
|
+
// The vault connection this profile grants (ADR-059). `main` when it grants
|
|
41
|
+
// none, which keeps a profile that denied the vault opening against the same
|
|
42
|
+
// document every other profile uses rather than inventing a second one.
|
|
43
|
+
const connection = soleGrantFor(config, 'vault') ?? 'main';
|
|
44
|
+
|
|
39
45
|
switch (vault.adapter) {
|
|
40
46
|
case 'file':
|
|
41
47
|
return createFileVaultStore({
|
|
42
|
-
path: workspacePath(root, vault.path ?? layout.vault(
|
|
48
|
+
path: workspacePath(root, vault.path ?? layout.vault(connection)),
|
|
43
49
|
});
|
|
44
50
|
|
|
45
51
|
case 'secret':
|
|
@@ -47,15 +53,24 @@ export function openVault(
|
|
|
47
53
|
// so the credential store holds ciphertext it cannot read. Separate
|
|
48
54
|
// document, separate key, separate environment variable — the backend
|
|
49
55
|
// was never what kept the two stores apart. ADR-022.
|
|
56
|
+
//
|
|
57
|
+
// Named per connection, like the file adapter. A `ref` written by hand
|
|
58
|
+
// still wins, because a deployment that already seals under one name has
|
|
59
|
+
// to keep opening it — but the default carries the instance, so two
|
|
60
|
+
// profiles granting different vaults share nothing. Without this only
|
|
61
|
+
// `file` honoured ADR-059 and every deployed workspace, which uses this
|
|
62
|
+
// adapter or `blob`, had one document behind every vault connection:
|
|
63
|
+
// ADR-059 calls that the worst of the three collisions, because the wrong
|
|
64
|
+
// answer is a credential.
|
|
50
65
|
return createSecretVaultStore({
|
|
51
66
|
store: credentials,
|
|
52
|
-
|
|
67
|
+
ref: vaultRef(declared, config),
|
|
53
68
|
});
|
|
54
69
|
|
|
55
70
|
case 'blob':
|
|
56
71
|
return createBlobVaultStore({
|
|
57
72
|
store: storage(),
|
|
58
|
-
|
|
73
|
+
key: vault.path ?? layout.vaultKey(connection),
|
|
59
74
|
});
|
|
60
75
|
}
|
|
61
76
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ConfigError, listProfiles, openTarget, resolveWorkspaceRoot } from '#profile';
|
|
2
|
+
import { openRuntime, type Runtime } from './open.ts';
|
|
3
|
+
import type { GlobalFlags } from './select.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A runtime for a workspace, when the command does not act on one profile.
|
|
7
|
+
*
|
|
8
|
+
* Contract 3 moved the credential store, the audit log, the state store and the
|
|
9
|
+
* blob root up to the workspace, so a command that opens any of them opens the
|
|
10
|
+
* same thing whichever profile it was handed. `openRuntime` still wants one,
|
|
11
|
+
* because a `Runtime` carries a resolved profile — so this picks one rather
|
|
12
|
+
* than making the caller ask a question with no answer.
|
|
13
|
+
*
|
|
14
|
+
* `--profile` still narrows where narrowing means something: the audit log is
|
|
15
|
+
* one chain and the flag filters it, rather than selecting which log to read.
|
|
16
|
+
* What it must never do is *decide* anything, which is why the profile chosen
|
|
17
|
+
* here when none is given is simply the first and is never reported as though
|
|
18
|
+
* it were the subject.
|
|
19
|
+
*/
|
|
20
|
+
export async function openWorkspaceRuntime(flags: GlobalFlags): Promise<Runtime> {
|
|
21
|
+
if (flags.profile !== undefined) return openRuntime(flags);
|
|
22
|
+
|
|
23
|
+
const root = resolveWorkspaceRoot();
|
|
24
|
+
const resolved = await openTarget(root, flags.target!);
|
|
25
|
+
const names = await listProfiles(resolved.workspaceRoot);
|
|
26
|
+
|
|
27
|
+
if (names.length === 0) {
|
|
28
|
+
throw new ConfigError(
|
|
29
|
+
`Workspace "${flags.target}" holds no profiles, so there is nothing to open.\n` +
|
|
30
|
+
` Create one with: lanes link profile add <name> --workspace ${flags.target}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return openRuntime({ ...flags, profile: names[0]! });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Which profile a workspace-level command should act *as*.
|
|
39
|
+
*
|
|
40
|
+
* Distinct from `openWorkspaceRuntime` because the caller needs the name rather
|
|
41
|
+
* than a runtime — `start` puts it back into flags and hands them on. Same rule:
|
|
42
|
+
* `--profile` wins, and otherwise the first is taken without being reported as
|
|
43
|
+
* a choice, because for the things this is used for it is not one.
|
|
44
|
+
*/
|
|
45
|
+
export async function primaryProfile(flags: GlobalFlags): Promise<string> {
|
|
46
|
+
if (flags.profile !== undefined) return flags.profile;
|
|
47
|
+
|
|
48
|
+
const root = resolveWorkspaceRoot();
|
|
49
|
+
const resolved = await openTarget(root, flags.target!);
|
|
50
|
+
const names = await listProfiles(resolved.workspaceRoot);
|
|
51
|
+
|
|
52
|
+
if (names.length === 0) {
|
|
53
|
+
throw new ConfigError(
|
|
54
|
+
`Workspace "${flags.target}" holds no profiles, so there is nothing to serve.\n` +
|
|
55
|
+
` Create one with: lanes link profile add <name> --workspace ${flags.target}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return names[0]!;
|
|
60
|
+
}
|
package/src/cli/runtime.ts
CHANGED
|
@@ -32,4 +32,5 @@ export {
|
|
|
32
32
|
type OwnerLayerOptions,
|
|
33
33
|
} from './runtime/registry.ts';
|
|
34
34
|
|
|
35
|
-
export { openRuntime, type OpenOptions, type Runtime } from './runtime/open.ts';
|
|
35
|
+
export { grantedConnections, openRuntime, type OpenOptions, type Runtime } from './runtime/open.ts';
|
|
36
|
+
export { openWorkspaceRuntime, primaryProfile } from './runtime/workspace.ts';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
listProfiles,
|
|
3
|
+
readWorkspace,
|
|
3
4
|
noProfileNamed,
|
|
4
5
|
noTargetNamed,
|
|
5
6
|
readRegistry,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
resolveWorkspaceRoot,
|
|
8
9
|
} from '#profile';
|
|
9
10
|
import type { Flags } from './argv.ts';
|
|
10
|
-
import { dispatchWillRefuse, requirementFor } from './selection.ts';
|
|
11
|
+
import { EXPLICIT_WORKSPACE, dispatchWillRefuse, requirementFor, selectionKey } from './selection.ts';
|
|
11
12
|
import { refuseIfUnmigrated } from './workspace-migrate.ts';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -46,26 +47,40 @@ export async function requireSelection(
|
|
|
46
47
|
|
|
47
48
|
const root = resolveWorkspaceRoot(env ? { env } : {});
|
|
48
49
|
|
|
49
|
-
// **
|
|
50
|
-
// and read that profile's own target list — the ordering ADR-052
|
|
51
|
-
// since
|
|
52
|
-
// one until the
|
|
53
|
-
// only, so it costs nothing when the flag is present.
|
|
54
|
-
if (typeof flags['
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
// **Workspace before profile, for both levels.** It used to ask for the
|
|
51
|
+
// profile and read that profile's own target list — the ordering ADR-052
|
|
52
|
+
// inverted, since a profile lives inside a workspace and there is nowhere to
|
|
53
|
+
// look for one until the workspace is known. `refuseIfUnmigrated` is on the
|
|
54
|
+
// refusal path only, so it costs nothing when the flag is present.
|
|
55
|
+
if (typeof flags['workspace'] !== 'string') {
|
|
56
|
+
const fallback = EXPLICIT_WORKSPACE.has(selectionKey(first, second))
|
|
57
|
+
? undefined
|
|
58
|
+
: (await readWorkspace(root))?.default_workspace;
|
|
59
|
+
|
|
60
|
+
if (fallback === undefined) {
|
|
61
|
+
await refuseIfUnmigrated(root);
|
|
62
|
+
throw noTargetNamed(await readRegistry(root), root, env, {
|
|
63
|
+
refusedDefault: EXPLICIT_WORKSPACE.has(selectionKey(first, second)),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Written back onto the flags so every command downstream reads one key and
|
|
68
|
+
// cannot tell a default from a typed flag — the difference belongs in the
|
|
69
|
+
// line printed above the output, not in thirty call sites (ADR-061).
|
|
70
|
+
flags['workspace'] = fallback;
|
|
71
|
+
defaulted.add(fallback);
|
|
57
72
|
}
|
|
58
73
|
|
|
59
|
-
// A named
|
|
60
|
-
if (!(flags['
|
|
74
|
+
// A named workspace the registry cannot know, because it does not exist yet.
|
|
75
|
+
if (!(flags['workspace'] in (await readRegistry(root)))) await refuseIfUnmigrated(root);
|
|
61
76
|
|
|
62
|
-
if (needs === '
|
|
77
|
+
if (needs === 'workspace') return;
|
|
63
78
|
|
|
64
79
|
if (typeof flags['profile'] !== 'string') {
|
|
65
80
|
// Listed from the target's own workspace, so the names offered are ones that
|
|
66
81
|
// command could act on. An unreachable pointer degrades to the empty list
|
|
67
82
|
// rather than failing — "which profile" is still the question being asked.
|
|
68
|
-
const where = await resolveTargetWorkspace(root, flags['
|
|
83
|
+
const where = await resolveTargetWorkspace(root, flags['workspace'] as string).catch(() => root);
|
|
69
84
|
throw noProfileNamed(where, await listProfiles(where).catch(() => []), env);
|
|
70
85
|
}
|
|
71
86
|
}
|
|
@@ -77,3 +92,19 @@ export async function requireSelection(
|
|
|
77
92
|
* that listing it per command would be noise. `--quiet` is read by `announce`
|
|
78
93
|
* rather than by any one command.
|
|
79
94
|
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Workspaces resolved from `default_workspace` rather than from a flag.
|
|
98
|
+
*
|
|
99
|
+
* A set rather than a boolean because `announce` is called from thirty places
|
|
100
|
+
* and none of them threads a resolution result through — and because the thing
|
|
101
|
+
* being remembered is small, true for one run, and read exactly once per line
|
|
102
|
+
* of output. If the echo is ever dropped for tidiness, ADR-061 has been
|
|
103
|
+
* reversed: the printed line is the whole of what makes a sticky default
|
|
104
|
+
* different from the dotfile nothing prints.
|
|
105
|
+
*/
|
|
106
|
+
const defaulted = new Set<string>();
|
|
107
|
+
|
|
108
|
+
export function wasDefaulted(workspace: string): boolean {
|
|
109
|
+
return defaulted.has(workspace);
|
|
110
|
+
}
|