@monosmith/plugin-sdk 0.3.0 → 0.5.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,12 +16,15 @@ 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');
22
23
  const made = [];
24
+ const declared = client.info.kinds ?? [];
23
25
  for (const kind of ['app', 'lib', 'package']) {
26
+ if (declared.length > 0 && !declared.includes(kind))
27
+ continue;
24
28
  const name = `c${kind}`;
25
29
  const params = {
26
30
  kind,
@@ -69,6 +73,9 @@ export async function checkGeneration(client, workspace, report) {
69
73
  if (client.supports('remove')) {
70
74
  await attempt(report, `would remove the ${params.kind} (dry run)`, () => client.call('remove', { project, dryRun: true }, { timeoutMs: 60_000 }));
71
75
  }
76
+ if (client.supports('project/kind')) {
77
+ await checkKindChange(client, workspace, project, projects, report);
78
+ }
72
79
  if (params.kind === 'package' && client.supports('publish')) {
73
80
  await attempt(report, 'publishes the package (dry run)', () => client.call('publish', { project, version: '1.2.3', dryRun: true }, { timeoutMs: 5 * 60_000 }));
74
81
  }
@@ -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.3.0",
3
+ "version": "0.5.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",
@@ -642,6 +642,10 @@ export const protocolSchema = {
642
642
  }
643
643
  },
644
644
  "description": "DependsOn adds dependencies to tasks the project already has, by task name ({\"lint\": [\"slice-check\"]})."
645
+ },
646
+ "release": {
647
+ "$ref": "#/$defs/ReleaseDefaults",
648
+ "description": "Release is how the project's release files are made, for a project that releases through GitHub releases and leaves them out of its smith.toml [projects.<name>.release] table; what the table sets wins."
645
649
  }
646
650
  }
647
651
  },
@@ -1163,6 +1167,13 @@ export const protocolSchema = {
1163
1167
  "$ref": "#/$defs/Stack"
1164
1168
  },
1165
1169
  "description": "Stacks are ready-made stacks the plugin publishes."
1170
+ },
1171
+ "kinds": {
1172
+ "type": "array",
1173
+ "items": {
1174
+ "$ref": "#/$defs/ProjectKind"
1175
+ },
1176
+ "description": "Kinds are the project kinds the plugin can scaffold with generate. Absent or empty means all three (app, lib and package): what every plugin that says nothing does. A plugin that scaffolds only some, such as an app, lists them, so the host offers and the conformance suite asks for just those."
1166
1177
  }
1167
1178
  },
1168
1179
  "required": [
@@ -1627,6 +1638,43 @@ export const protocolSchema = {
1627
1638
  "auth"
1628
1639
  ]
1629
1640
  },
1641
+ "ReleaseDefaults": {
1642
+ "description": "ReleaseDefaults are release settings a plugin supplies for a project: the same fields as the release-from-a-command settings of smith.toml, used for the ones the project's table leaves out.",
1643
+ "type": "object",
1644
+ "properties": {
1645
+ "build": {
1646
+ "type": "string",
1647
+ "description": "Build is the shell command that makes the files, run at the workspace root on a runner of each platform."
1648
+ },
1649
+ "assets": {
1650
+ "type": "array",
1651
+ "items": {
1652
+ "type": "string"
1653
+ },
1654
+ "description": "Assets are workspace-relative globs of the files Build leaves."
1655
+ },
1656
+ "platforms": {
1657
+ "type": "array",
1658
+ "items": {
1659
+ "type": "string"
1660
+ },
1661
+ "description": "Platforms are the runner platforms (windows-x64, macos-arm64, macos-x64, linux-x64, linux-arm64)."
1662
+ },
1663
+ "assetName": {
1664
+ "type": "string",
1665
+ "description": "AssetName renames each file on upload; see the asset_name setting."
1666
+ },
1667
+ "checksums": {
1668
+ "type": "boolean",
1669
+ "description": "Checksums attaches a checksums file for the files a runner uploads."
1670
+ }
1671
+ },
1672
+ "required": [
1673
+ "build",
1674
+ "assets",
1675
+ "platforms"
1676
+ ]
1677
+ },
1630
1678
  "RemoveParams": {
1631
1679
  "description": "RemoveParams tells a plugin a project is being removed. The host deletes the project's directory first; the plugin cleans what it owns outside it (a go.work entry, a lockfile, an installed package).",
1632
1680
  "type": "object",
@@ -229,6 +229,12 @@ export interface Contribution {
229
229
  * ["slice-check"]}).
230
230
  */
231
231
  dependsOn?: Record<string, string[]>;
232
+ /**
233
+ * Release is how the project's release files are made, for a project that releases through GitHub
234
+ * releases and leaves them out of its smith.toml [projects.<name>.release] table; what the table
235
+ * sets wins.
236
+ */
237
+ release?: ReleaseDefaults;
232
238
  }
233
239
  /** Credential is a secret the release phase needs. */
234
240
  export interface Credential {
@@ -491,6 +497,13 @@ export interface InitializeResult {
491
497
  conflicts?: PluginRef[];
492
498
  /** Stacks are ready-made stacks the plugin publishes. */
493
499
  stacks?: Stack[];
500
+ /**
501
+ * Kinds are the project kinds the plugin can scaffold with generate. Absent or empty means all
502
+ * three (app, lib and package): what every plugin that says nothing does. A plugin that scaffolds
503
+ * only some, such as an app, lists them, so the host offers and the conformance suite asks for
504
+ * just those.
505
+ */
506
+ kinds?: ProjectKind[];
494
507
  }
495
508
  /**
496
509
  * Input is one input of an ActionStep. Values are written as quoted YAML strings, except true and
@@ -796,6 +809,28 @@ export interface Registry {
796
809
  */
797
810
  maven?: string;
798
811
  }
812
+ /**
813
+ * ReleaseDefaults are release settings a plugin supplies for a project: the same fields as the
814
+ * release-from-a-command settings of smith.toml, used for the ones the project's table leaves out.
815
+ */
816
+ export interface ReleaseDefaults {
817
+ /**
818
+ * Build is the shell command that makes the files, run at the workspace root on a runner of each
819
+ * platform.
820
+ */
821
+ build: string;
822
+ /** Assets are workspace-relative globs of the files Build leaves. */
823
+ assets: string[];
824
+ /**
825
+ * Platforms are the runner platforms (windows-x64, macos-arm64, macos-x64, linux-x64,
826
+ * linux-arm64).
827
+ */
828
+ platforms: string[];
829
+ /** AssetName renames each file on upload; see the asset_name setting. */
830
+ assetName?: string;
831
+ /** Checksums attaches a checksums file for the files a runner uploads. */
832
+ checksums?: boolean;
833
+ }
799
834
  /**
800
835
  * RemoveParams tells a plugin a project is being removed. The host deletes the project's directory
801
836
  * first; the plugin cleans what it owns outside it (a go.work entry, a lockfile, an installed