@hydranium/cli 1.0.0-next.117 → 1.0.0-next.123

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.
@@ -743,9 +743,9 @@ const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's
743
743
  //
744
744
  // Without it every structured write fails: the framework's default binding at
745
745
  // \`services.serializer.Serializer\` THROWS, because a concrete syntax is
746
- // language knowledge no framework can derive. \`ModelService.update\` / \`save\`,
747
- // the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\` all reach
748
- // it.
746
+ // language knowledge no framework can derive. A client session's \`update\` /
747
+ // \`save\`, the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
748
+ // all reach it.
749
749
  //
750
750
  // \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
751
751
  // output — its YAML and JSON subclasses are what it exists for — and cannot
@@ -1172,8 +1172,8 @@ function serializationTest(composition) {
1172
1172
  const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
1173
1173
  const text = `// Round-trips each grammar through its serializer: parse the source, serialize
1174
1174
  // the model, compare the text. The serializer is the parser's inverse, so this
1175
- // is the assertion that keeps \`ModelService.update\` / \`save\` writing files the
1176
- // language server can read back.
1175
+ // is the assertion that keeps a client session's \`update\` / \`save\` writing
1176
+ // files the language server can read back.
1177
1177
  //
1178
1178
  // The transfer case is the one that catches the mistake worth catching.
1179
1179
  // \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
@@ -18,6 +18,11 @@ import type { DataServerProtocol } from '@hydranium/protocol/data';
18
18
  *
19
19
  * `clientId` defaults to `'hydranium-cli'` — adopters wanting a richer
20
20
  * identity (per-user, per-script) override.
21
+ *
22
+ * The command saves as a client session under `clientId`: it registers the
23
+ * session, creates the document from the content when there is no file,
24
+ * opens it otherwise, saves, and ends the session, which leaves nothing open
25
+ * behind it.
21
26
  */
22
27
  export interface SaveCommandOptions {
23
28
  readonly serverCommand: string;
@@ -1 +1 @@
1
- {"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAKnE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;IAC9D,sFAAsF;IACtF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACjE;AAED,wBAAsB,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBxE"}
1
+ {"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAKnE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,kBAAkB;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;IAC9D,sFAAsF;IACtF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACjE;AAED,wBAAsB,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBxE"}
@@ -14,8 +14,7 @@ export async function runSave(options) {
14
14
  const clientId = options.clientId ?? 'hydranium-cli';
15
15
  const model = await resolveContent(options.content, options.__readFileForTest);
16
16
  if (options.__proxyForTest) {
17
- const doc = await options.__proxyForTest.saveModelDocument({ uri: options.uri, clientId, model, basedOn: 'anything' });
18
- write(`${JSON.stringify(doc)}\n`);
17
+ write(`${JSON.stringify(await saveOpened(options.__proxyForTest, options.uri, clientId, model))}\n`);
19
18
  return;
20
19
  }
21
20
  await withDataServer({
@@ -24,10 +23,36 @@ export async function runSave(options) {
24
23
  cwd: options.cwd,
25
24
  env: options.logLevel ? logLevelEnv(options.logLevel) : undefined
26
25
  }, async (server) => {
27
- const doc = await server.saveModelDocument({ uri: options.uri, clientId, model, basedOn: 'anything' });
28
- write(`${JSON.stringify(doc)}\n`);
26
+ write(`${JSON.stringify(await saveOpened(server, options.uri, clientId, model))}\n`);
29
27
  });
30
28
  }
29
+ /**
30
+ * Save `model` to `uri` as the session `clientId`, and end the session whether
31
+ * the save succeeded or not. An end that fails after a failed save is
32
+ * swallowed: the save's error is the one the user needs.
33
+ */
34
+ async function saveOpened(server, uri, clientId, model) {
35
+ await server.createSession({ clientId, label: 'hydranium-cli' });
36
+ let saved;
37
+ try {
38
+ // The server creates a document only when it finds no file and no
39
+ // client has it open; anything else is opened. An open that fails
40
+ // reports why the create failed too, since that may be the real cause.
41
+ await server.createModelDocument({ uri, clientId, text: model }).catch((created) => server.openModelDocument({ uri, clientId }).catch((opened) => {
42
+ throw new Error(`Cannot save ${uri}: ${describe(opened)} (creating it failed first: ${describe(created)})`, { cause: created });
43
+ }));
44
+ saved = await server.saveModelDocument({ uri, clientId, model, basedOn: 'anything' });
45
+ }
46
+ catch (error) {
47
+ await server.closeSession({ clientId }).catch(() => undefined);
48
+ throw error;
49
+ }
50
+ await server.closeSession({ clientId });
51
+ return saved;
52
+ }
53
+ function describe(error) {
54
+ return error instanceof Error ? error.message : String(error);
55
+ }
31
56
  /**
32
57
  * Resolve the `--content` argument to its textual form. The `@<path>`
33
58
  * prefix reads from a file (parses as UTF-8); anything else is taken
@@ -1 +1 @@
1
- {"version":3,"file":"save.js","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAIlF,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AA6BzD,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,OAA2B;IACtD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9E,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,eAAe,CAAC;IACrD,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAE/E,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,iBAAiB,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;QACvH,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,OAAO;IACV,CAAC;IAED,MAAM,cAAc,CACjB;QACG,OAAO,EAAE,OAAO,CAAC,aAAa;QAC9B,IAAI,EAAE,OAAO,CAAC,UAAU;QACxB,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;KACnE,EACD,KAAK,EAAC,MAAM,EAAC,EAAE;QACZ,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;QACvG,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC,CACH,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,cAAc,CAAC,OAAe,EAAE,QAA4C;IACxF,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,MAAM,GAAG,QAAQ,IAAI,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAC7E,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;IACD,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,OAAO,CAAC;AAClB,CAAC"}
1
+ {"version":3,"file":"save.js","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydranium/cli",
3
- "version": "1.0.0-next.117",
3
+ "version": "1.0.0-next.123",
4
4
  "description": "Build-time codegen tools + data-server subcommands for the hydranium framework.",
5
5
  "keywords": [
6
6
  "hydranium",
@@ -65,20 +65,20 @@
65
65
  "ts-morph": "^25.0.0"
66
66
  },
67
67
  "devDependencies": {
68
- "@hydranium/core": "1.0.0-next.117",
69
- "@hydranium/data-server": "1.0.0-next.117",
70
- "@hydranium/langium": "1.0.0-next.117",
71
- "@hydranium/protocol": "1.0.0-next.117",
68
+ "@hydranium/core": "1.0.0-next.123",
69
+ "@hydranium/data-server": "1.0.0-next.123",
70
+ "@hydranium/langium": "1.0.0-next.123",
71
+ "@hydranium/protocol": "1.0.0-next.123",
72
72
  "rimraf": "^5.0.0",
73
73
  "typescript": "^5.8.0",
74
74
  "vscode-jsonrpc": "9.0.1",
75
75
  "vscode-languageserver": "~10.0.1"
76
76
  },
77
77
  "peerDependencies": {
78
- "@hydranium/core": "1.0.0-next.117",
79
- "@hydranium/data-server": "1.0.0-next.117",
80
- "@hydranium/langium": "1.0.0-next.117",
81
- "@hydranium/protocol": "1.0.0-next.117",
78
+ "@hydranium/core": "1.0.0-next.123",
79
+ "@hydranium/data-server": "1.0.0-next.123",
80
+ "@hydranium/langium": "1.0.0-next.123",
81
+ "@hydranium/protocol": "1.0.0-next.123",
82
82
  "@memlab/core": "^2.0.3",
83
83
  "@memlab/heap-analysis": "^2.0.3",
84
84
  "vscode-jsonrpc": "9.0.1"
@@ -809,9 +809,9 @@ const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's
809
809
  //
810
810
  // Without it every structured write fails: the framework's default binding at
811
811
  // \`services.serializer.Serializer\` THROWS, because a concrete syntax is
812
- // language knowledge no framework can derive. \`ModelService.update\` / \`save\`,
813
- // the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\` all reach
814
- // it.
812
+ // language knowledge no framework can derive. A client session's \`update\` /
813
+ // \`save\`, the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
814
+ // all reach it.
815
815
  //
816
816
  // \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
817
817
  // output — its YAML and JSON subclasses are what it exists for — and cannot
@@ -1260,8 +1260,8 @@ function serializationTest(composition: InitComposition): string {
1260
1260
  const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
1261
1261
  const text = `// Round-trips each grammar through its serializer: parse the source, serialize
1262
1262
  // the model, compare the text. The serializer is the parser's inverse, so this
1263
- // is the assertion that keeps \`ModelService.update\` / \`save\` writing files the
1264
- // language server can read back.
1263
+ // is the assertion that keeps a client session's \`update\` / \`save\` writing
1264
+ // files the language server can read back.
1265
1265
  //
1266
1266
  // The transfer case is the one that catches the mistake worth catching.
1267
1267
  // \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
@@ -23,6 +23,11 @@ import { withDataServer } from '../spawn-data-server.js';
23
23
  *
24
24
  * `clientId` defaults to `'hydranium-cli'` — adopters wanting a richer
25
25
  * identity (per-user, per-script) override.
26
+ *
27
+ * The command saves as a client session under `clientId`: it registers the
28
+ * session, creates the document from the content when there is no file,
29
+ * opens it otherwise, saves, and ends the session, which leaves nothing open
30
+ * behind it.
26
31
  */
27
32
  export interface SaveCommandOptions {
28
33
  readonly serverCommand: string;
@@ -46,8 +51,7 @@ export async function runSave(options: SaveCommandOptions): Promise<void> {
46
51
  const model = await resolveContent(options.content, options.__readFileForTest);
47
52
 
48
53
  if (options.__proxyForTest) {
49
- const doc = await options.__proxyForTest.saveModelDocument({ uri: options.uri, clientId, model, basedOn: 'anything' });
50
- write(`${JSON.stringify(doc)}\n`);
54
+ write(`${JSON.stringify(await saveOpened(options.__proxyForTest, options.uri, clientId, model))}\n`);
51
55
  return;
52
56
  }
53
57
 
@@ -59,12 +63,49 @@ export async function runSave(options: SaveCommandOptions): Promise<void> {
59
63
  env: options.logLevel ? logLevelEnv(options.logLevel) : undefined
60
64
  },
61
65
  async server => {
62
- const doc = await server.saveModelDocument({ uri: options.uri, clientId, model, basedOn: 'anything' });
63
- write(`${JSON.stringify(doc)}\n`);
66
+ write(`${JSON.stringify(await saveOpened(server, options.uri, clientId, model))}\n`);
64
67
  }
65
68
  );
66
69
  }
67
70
 
71
+ /**
72
+ * Save `model` to `uri` as the session `clientId`, and end the session whether
73
+ * the save succeeded or not. An end that fails after a failed save is
74
+ * swallowed: the save's error is the one the user needs.
75
+ */
76
+ async function saveOpened(
77
+ server: Pick<
78
+ DataServerProtocol<TransferElement>,
79
+ 'createSession' | 'openModelDocument' | 'createModelDocument' | 'saveModelDocument' | 'closeSession'
80
+ >,
81
+ uri: string,
82
+ clientId: string,
83
+ model: string
84
+ ): Promise<unknown> {
85
+ await server.createSession({ clientId, label: 'hydranium-cli' });
86
+ let saved: unknown;
87
+ try {
88
+ // The server creates a document only when it finds no file and no
89
+ // client has it open; anything else is opened. An open that fails
90
+ // reports why the create failed too, since that may be the real cause.
91
+ await server.createModelDocument({ uri, clientId, text: model }).catch((created: unknown) =>
92
+ server.openModelDocument({ uri, clientId }).catch((opened: unknown) => {
93
+ throw new Error(`Cannot save ${uri}: ${describe(opened)} (creating it failed first: ${describe(created)})`, { cause: created });
94
+ })
95
+ );
96
+ saved = await server.saveModelDocument({ uri, clientId, model, basedOn: 'anything' });
97
+ } catch (error: unknown) {
98
+ await server.closeSession({ clientId }).catch(() => undefined);
99
+ throw error;
100
+ }
101
+ await server.closeSession({ clientId });
102
+ return saved;
103
+ }
104
+
105
+ function describe(error: unknown): string {
106
+ return error instanceof Error ? error.message : String(error);
107
+ }
108
+
68
109
  /**
69
110
  * Resolve the `--content` argument to its textual form. The `@<path>`
70
111
  * prefix reads from a file (parses as UTF-8); anything else is taken