@monosmith/plugin-sdk 0.2.1 → 0.4.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.
@@ -7,6 +7,6 @@ export declare const CONFORMANCE_STACK = "conformance";
7
7
  * exist, and, for a language plugin, that its own `discover` returns what it
8
8
  * made, with the kind and the stack it was asked for, tasks with commands, and
9
9
  * that the dry runs of `version/write`, `remove`, `publish` and `published`
10
- * answer.
10
+ * answer, and, for a plugin that changes kinds, what `project/kind` edits.
11
11
  */
12
12
  export declare function checkGeneration(client: PluginClient, workspace: string, report: ConformanceReport): Promise<void>;
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { effectiveRoles } from '../plugin-roles/index.js';
4
4
  import { projectKindDirectory } from '../projects/index.js';
5
+ import { checkKindChange } from './check-kind-change.use-case.js';
5
6
  import { findManifests } from './find-manifests.algorithm.js';
6
7
  import { goList, goQuote } from './go-list.mapper.js';
7
8
  /** The stack name generated projects are asked to record. */
@@ -15,7 +16,7 @@ function article(kind) {
15
16
  * exist, and, for a language plugin, that its own `discover` returns what it
16
17
  * made, with the kind and the stack it was asked for, tasks with commands, and
17
18
  * that the dry runs of `version/write`, `remove`, `publish` and `published`
18
- * answer.
19
+ * answer, and, for a plugin that changes kinds, what `project/kind` edits.
19
20
  */
20
21
  export async function checkGeneration(client, workspace, report) {
21
22
  const language = effectiveRoles(client.info).includes('language');
@@ -69,6 +70,9 @@ export async function checkGeneration(client, workspace, report) {
69
70
  if (client.supports('remove')) {
70
71
  await attempt(report, `would remove the ${params.kind} (dry run)`, () => client.call('remove', { project, dryRun: true }, { timeoutMs: 60_000 }));
71
72
  }
73
+ if (client.supports('project/kind')) {
74
+ await checkKindChange(client, workspace, project, projects, report);
75
+ }
72
76
  if (params.kind === 'package' && client.supports('publish')) {
73
77
  await attempt(report, 'publishes the package (dry run)', () => client.call('publish', { project, version: '1.2.3', dryRun: true }, { timeoutMs: 5 * 60_000 }));
74
78
  }
@@ -0,0 +1,9 @@
1
+ import type { PluginClient } from '../host/index.ts';
2
+ import type { Project } from '../protocol/index.ts';
3
+ import type { ConformanceReport } from './conformance-report.model.ts';
4
+ /**
5
+ * Asks a plugin that declares `changeKind` what making a generated project
6
+ * another kind edits, and checks it answers with a refusal or with edits to
7
+ * files that exist, and writes nothing itself: the host applies the edits.
8
+ */
9
+ export declare function checkKindChange(client: PluginClient, workspace: string, project: Project, all: readonly Project[], report: ConformanceReport): Promise<void>;
@@ -0,0 +1,58 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join, posix } from 'node:path';
3
+ import { projectKindDirectory } from '../projects/index.js';
4
+ import { findManifests } from './find-manifests.algorithm.js';
5
+ import { goList } from './go-list.mapper.js';
6
+ /** The kind a generated project is asked to become: a lib or an app is published as a package, a package goes back to a lib. */
7
+ function kindChangeTarget(kind) {
8
+ if (kind === 'app')
9
+ return 'lib';
10
+ return kind === 'lib' ? 'package' : 'lib';
11
+ }
12
+ function article(kind) {
13
+ return kind === 'app' ? 'an app' : `a ${kind}`;
14
+ }
15
+ /** The content of every manifest of the workspace, by path. */
16
+ function snapshot(workspace, names) {
17
+ const content = new Map();
18
+ for (const manifest of findManifests(workspace, names)) {
19
+ const text = readFileSync(join(workspace, ...manifest.split('/')), 'utf8');
20
+ content.set(manifest, text);
21
+ }
22
+ return content;
23
+ }
24
+ function same(a, b) {
25
+ return a.size === b.size && [...a].every(([path, text]) => b.get(path) === text);
26
+ }
27
+ /**
28
+ * Asks a plugin that declares `changeKind` what making a generated project
29
+ * another kind edits, and checks it answers with a refusal or with edits to
30
+ * files that exist, and writes nothing itself: the host applies the edits.
31
+ */
32
+ export async function checkKindChange(client, workspace, project, all, report) {
33
+ const to = kindChangeTarget(project.kind);
34
+ const newRoot = `${projectKindDirectory(to)}/${posix.basename(project.root)}`;
35
+ const check = `answers what making the ${project.kind} ${article(to)} edits (dry run)`;
36
+ const names = client.info.manifests ?? [];
37
+ const before = snapshot(workspace, names);
38
+ let answer;
39
+ try {
40
+ answer = await client.call('project/kind', { project, to, newRoot, projects: [...all] }, { timeoutMs: 5 * 60_000 });
41
+ }
42
+ catch (error) {
43
+ report.add(check, false, error instanceof Error ? error.message : String(error));
44
+ return;
45
+ }
46
+ const files = answer.files ?? [];
47
+ const missing = files.map(file => file.path).filter(path => !existsSync(join(workspace, ...path.split('/'))));
48
+ if ((answer.refused ?? '') === '' && files.length === 0) {
49
+ report.add(check, false, 'it answered with neither a refusal nor an edit: a changed kind is recorded in a file');
50
+ }
51
+ else if (missing.length > 0) {
52
+ report.add(check, false, `edits files that do not exist (paths are those before the move): ${goList(missing)}`);
53
+ }
54
+ else {
55
+ const untouched = same(before, snapshot(workspace, names));
56
+ report.add(check, untouched, 'it wrote to the workspace: it must only answer with the new content, and let the host write it');
57
+ }
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monosmith/plugin-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "The TypeScript SDK for writing smith plugins: the protocol's types, the serve loop, a test host, the conformance checks, templating and key-order-keeping JSON editing. No runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -131,6 +131,17 @@ export const protocolSchema = {
131
131
  "capability": "remove",
132
132
  "notification": false
133
133
  },
134
+ "project/kind": {
135
+ "description": "Asks what changing a project between app, lib and package edits: its manifest, the workspace files and the dependents that name its kind or directory. The host applies the edits and moves the directory.",
136
+ "params": {
137
+ "$ref": "#/$defs/ChangeKindParams"
138
+ },
139
+ "result": {
140
+ "$ref": "#/$defs/ChangeKindResult"
141
+ },
142
+ "capability": "changeKind",
143
+ "notification": false
144
+ },
134
145
  "published": {
135
146
  "description": "Asks which version of a project its registry has, so a release can check its tags against it.",
136
147
  "params": {
@@ -497,6 +508,10 @@ export const protocolSchema = {
497
508
  "type": "boolean",
498
509
  "description": "Remove cleans up a removed project's traces outside its directory."
499
510
  },
511
+ "changeKind": {
512
+ "type": "boolean",
513
+ "description": "ChangeKind answers what changing a project's kind edits (smith kind)."
514
+ },
500
515
  "published": {
501
516
  "type": "boolean",
502
517
  "description": "Published reports what a registry has of a project."
@@ -519,6 +534,60 @@ export const protocolSchema = {
519
534
  }
520
535
  }
521
536
  },
537
+ "ChangeKindParams": {
538
+ "description": "ChangeKindParams asks a plugin what changing a project's kind edits. The plugin writes nothing: it answers with the files' new contents, and the host applies them, then moves the project's directory.",
539
+ "type": "object",
540
+ "properties": {
541
+ "project": {
542
+ "$ref": "#/$defs/Project",
543
+ "description": "Project is the project as it is now."
544
+ },
545
+ "to": {
546
+ "$ref": "#/$defs/ProjectKind",
547
+ "description": "To is the kind it becomes."
548
+ },
549
+ "newRoot": {
550
+ "type": "string",
551
+ "description": "NewRoot is the workspace-relative directory the host moves the project to afterwards; it equals Project.Root when the directory stays."
552
+ },
553
+ "projects": {
554
+ "type": "array",
555
+ "items": {
556
+ "$ref": "#/$defs/Project"
557
+ },
558
+ "description": "Projects are every project of the workspace, so the plugin can find the ones whose manifests and sources name this one."
559
+ }
560
+ },
561
+ "required": [
562
+ "project",
563
+ "to",
564
+ "newRoot"
565
+ ]
566
+ },
567
+ "ChangeKindResult": {
568
+ "description": "ChangeKindResult is what the change edits. Every path in Files is the path the file has before the move.",
569
+ "type": "object",
570
+ "properties": {
571
+ "refused": {
572
+ "type": "string",
573
+ "description": "Refused says why this kind makes no sense for the project's language (\"a Bicep template is deployed, never published\"); the host stops and shows it. Empty when the change is fine."
574
+ },
575
+ "files": {
576
+ "type": "array",
577
+ "items": {
578
+ "$ref": "#/$defs/FileEdit"
579
+ },
580
+ "description": "Files are the edits: manifests, workspace files and dependents that name the project's kind or its directory."
581
+ },
582
+ "warnings": {
583
+ "type": "array",
584
+ "items": {
585
+ "type": "string"
586
+ },
587
+ "description": "Warnings name what the change loses or leaves for the user (a binary an app has and a lib does not, a lockfile to refresh)."
588
+ }
589
+ }
590
+ },
522
591
  "ContributeParams": {
523
592
  "description": "ContributeParams hands a plugin the projects it applies to: those whose stack includes it, or, for a workspace-wide convention, those of the languages it supports that smith.toml enables it for.",
524
593
  "type": "object",
@@ -1341,6 +1410,10 @@ export const protocolSchema = {
1341
1410
  "registries": {
1342
1411
  "$ref": "#/$defs/Registries",
1343
1412
  "description": "Registries are the private registries the project installs from and publishes to, from smith.toml [registries]; nil means the public ones. Set by the host."
1413
+ },
1414
+ "releaseAssets": {
1415
+ "$ref": "#/$defs/AssetSpec",
1416
+ "description": "ReleaseAssets is what an app's release builds, from smith.toml [projects.<project>.release]: the platforms and file names, so a plugin can check (doctor) and prepare (CI) for them. Nil for a project that has no release files. Set by the host."
1344
1417
  }
1345
1418
  },
1346
1419
  "required": [
@@ -144,6 +144,8 @@ export interface Capabilities {
144
144
  contribute?: boolean;
145
145
  /** Remove cleans up a removed project's traces outside its directory. */
146
146
  remove?: boolean;
147
+ /** ChangeKind answers what changing a project's kind edits (smith kind). */
148
+ changeKind?: boolean;
147
149
  /** Published reports what a registry has of a project. */
148
150
  published?: boolean;
149
151
  /** Dependencies installs and uninstalls packages in its projects. */
@@ -159,6 +161,48 @@ export interface Capabilities {
159
161
  */
160
162
  cancel?: boolean;
161
163
  }
164
+ /**
165
+ * ChangeKindParams asks a plugin what changing a project's kind edits. The plugin writes nothing:
166
+ * it answers with the files' new contents, and the host applies them, then moves the project's
167
+ * directory.
168
+ */
169
+ export interface ChangeKindParams {
170
+ /** Project is the project as it is now. */
171
+ project: Project;
172
+ /** To is the kind it becomes. */
173
+ to: ProjectKind;
174
+ /**
175
+ * NewRoot is the workspace-relative directory the host moves the project to afterwards; it equals
176
+ * Project.Root when the directory stays.
177
+ */
178
+ newRoot: string;
179
+ /**
180
+ * Projects are every project of the workspace, so the plugin can find the ones whose manifests
181
+ * and sources name this one.
182
+ */
183
+ projects?: Project[];
184
+ }
185
+ /**
186
+ * ChangeKindResult is what the change edits. Every path in Files is the path the file has before
187
+ * the move.
188
+ */
189
+ export interface ChangeKindResult {
190
+ /**
191
+ * Refused says why this kind makes no sense for the project's language ("a Bicep template is
192
+ * deployed, never published"); the host stops and shows it. Empty when the change is fine.
193
+ */
194
+ refused?: string;
195
+ /**
196
+ * Files are the edits: manifests, workspace files and dependents that name the project's kind or
197
+ * its directory.
198
+ */
199
+ files?: FileEdit[];
200
+ /**
201
+ * Warnings name what the change loses or leaves for the user (a binary an app has and a lib does
202
+ * not, a lockfile to refresh).
203
+ */
204
+ warnings?: string[];
205
+ }
162
206
  /**
163
207
  * ContributeParams hands a plugin the projects it applies to: those whose stack includes it, or,
164
208
  * for a workspace-wide convention, those of the languages it supports that smith.toml enables it
@@ -611,6 +655,12 @@ export interface Project {
611
655
  * smith.toml [registries]; nil means the public ones. Set by the host.
612
656
  */
613
657
  registries?: Registries;
658
+ /**
659
+ * ReleaseAssets is what an app's release builds, from smith.toml [projects.<project>.release]:
660
+ * the platforms and file names, so a plugin can check (doctor) and prepare (CI) for them. Nil for
661
+ * a project that has no release files. Set by the host.
662
+ */
663
+ releaseAssets?: AssetSpec;
614
664
  }
615
665
  /**
616
666
  * ProjectKind is the universal kind vocabulary.
@@ -1019,6 +1069,16 @@ export interface ProtocolMethods {
1019
1069
  result: RemoveResult;
1020
1070
  capability: 'remove';
1021
1071
  };
1072
+ /**
1073
+ * Asks what changing a project between app, lib and package edits: its manifest, the workspace
1074
+ * files and the dependents that name its kind or directory. The host applies the edits and moves
1075
+ * the directory.
1076
+ */
1077
+ 'project/kind': {
1078
+ params: ChangeKindParams;
1079
+ result: ChangeKindResult;
1080
+ capability: 'changeKind';
1081
+ };
1022
1082
  /** Asks which version of a project its registry has, so a release can check its tags against it. */
1023
1083
  published: {
1024
1084
  params: PublishedParams;
package/serve/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { serve, serveStreams } from './serve.use-case.ts';
2
2
  export { capabilitiesOf, implementsMethod } from './capabilities-of.algorithm.ts';
3
- export type { Awaitable, Cancellable, CIContributor, Contributor, DeclarationEditor, DependencyManager, Discoverer, DoctorContributor, Generator, LatestReader, Plugin, PluginDescription, Publisher, RegistryReader, Remover, SmithPlugin, Versioner, } from './plugin.contract.ts';
3
+ export type { Awaitable, Cancellable, CIContributor, Contributor, DeclarationEditor, DependencyManager, Discoverer, DoctorContributor, Generator, KindChanger, LatestReader, Plugin, PluginDescription, Publisher, RegistryReader, Remover, SmithPlugin, Versioner, } from './plugin.contract.ts';
4
4
  export type { FrameSink, ServeStreams } from './protocol-streams.contract.ts';
@@ -16,6 +16,7 @@ export const PLUGIN_MEMBERS = {
16
16
  'doctor': { member: 'doctor', requires: ['doctor'] },
17
17
  'contribute': { member: 'contribute', requires: ['contribute'] },
18
18
  'remove': { member: 'remove', requires: ['remove'] },
19
+ 'project/kind': { member: 'changeKind', requires: ['changeKind'] },
19
20
  'published': { member: 'published', requires: ['published'] },
20
21
  'dependencies/install': { member: 'install', requires: ['install', 'uninstall'] },
21
22
  'dependencies/uninstall': { member: 'uninstall', requires: ['install', 'uninstall'] },
@@ -1,4 +1,4 @@
1
- import type { CIParams, CIResult, ContributeParams, ContributeResult, DeclaredParams, DeclaredResult, DependencyResult, DiscoverParams, DiscoverResult, DoctorParams, DoctorResult, GenerateParams, GenerateResult, InitializeParams, InitializeResult, InstallParams, LatestParams, LatestResult, PublishedParams, PublishedResult, PublishParams, PublishResult, RemoveParams, RemoveResult, RewriteParams, UninstallParams, VersionParams, VersionResult } from '../protocol/index.ts';
1
+ import type { ChangeKindParams, ChangeKindResult, CIParams, CIResult, ContributeParams, ContributeResult, DeclaredParams, DeclaredResult, DependencyResult, DiscoverParams, DiscoverResult, DoctorParams, DoctorResult, GenerateParams, GenerateResult, InitializeParams, InitializeResult, InstallParams, LatestParams, LatestResult, PublishedParams, PublishedResult, PublishParams, PublishResult, RemoveParams, RemoveResult, RewriteParams, UninstallParams, VersionParams, VersionResult } from '../protocol/index.ts';
2
2
  /** A result now or later: every method of a plugin may be `async`. */
3
3
  export type Awaitable<T> = T | Promise<T>;
4
4
  /** What a plugin says about itself. The serve loop fills in `protocolVersion` and `capabilities`. */
@@ -40,6 +40,10 @@ export interface DoctorContributor {
40
40
  export interface Remover {
41
41
  remove: (params: RemoveParams, signal: AbortSignal) => Awaitable<RemoveResult>;
42
42
  }
43
+ /** `project/kind`: what changing a project between app, lib and package edits. */
44
+ export interface KindChanger {
45
+ changeKind: (params: ChangeKindParams, signal: AbortSignal) => Awaitable<ChangeKindResult>;
46
+ }
43
47
  /** `dependencies/install` and `dependencies/uninstall`: both, or the capability is not advertised. */
44
48
  export interface DependencyManager {
45
49
  install: (params: InstallParams, signal: AbortSignal) => Awaitable<DependencyResult>;
@@ -77,4 +81,4 @@ export interface Cancellable {
77
81
  * from `cancellable`. How many requests it can work on at once is declared in
78
82
  * `describe`'s `concurrency`: omit it for one at a time, in order.
79
83
  */
80
- export type SmithPlugin = Plugin & Partial<Discoverer & Generator & Versioner & Publisher & RegistryReader & CIContributor & DoctorContributor & Remover & DependencyManager & DeclarationEditor & LatestReader & Contributor & Cancellable>;
84
+ export type SmithPlugin = Plugin & Partial<Discoverer & Generator & Versioner & Publisher & RegistryReader & CIContributor & DoctorContributor & Remover & KindChanger & DependencyManager & DeclarationEditor & LatestReader & Contributor & Cancellable>;