@gjsify/cli 0.64.13-next.283 → 0.64.14

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/lib/cli-app.js CHANGED
@@ -18,7 +18,7 @@
18
18
  import yargs from 'yargs';
19
19
  import { classifyCliFailure } from './cli-fail.js';
20
20
  import { cliVersion } from './utils/publish-headers.js';
21
- import { buildCommand as build, clearCommand as clear, copyCommand as copy, envCommand as env, execCommand as exec, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, siteCommand as site, devCommand as dev, debugCommand as debug, devtoolsCommand as devtools, browseCommand as browse, webextCommand as webext, blueprintCommand as blueprint, } from './commands/index.js';
21
+ import { buildCommand as build, clearCommand as clear, copyCommand as copy, envCommand as env, execCommand as exec, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, releaseNotesCommand as releaseNotes, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, siteCommand as site, devCommand as dev, debugCommand as debug, devtoolsCommand as devtools, browseCommand as browse, webextCommand as webext, blueprintCommand as blueprint, } from './commands/index.js';
22
22
  import { APP_NAME } from './constants.js';
23
23
  import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
24
24
  import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
@@ -117,6 +117,7 @@ export async function runCli(argv) {
117
117
  .command(pack.command, pack.description, pack.builder, pack.handler)
118
118
  .command(publish.command, publish.description, publish.builder, publish.handler)
119
119
  .command(whoami.command, whoami.description, whoami.builder, whoami.handler)
120
+ .command(releaseNotes.command, releaseNotes.description, releaseNotes.builder, releaseNotes.handler)
120
121
  .command(login.command, login.description, login.builder, login.handler)
121
122
  .command(logout.command, logout.description, logout.builder, logout.handler)
122
123
  .command(trust.command, trust.description, trust.builder, trust.handler)
@@ -22,6 +22,7 @@ export * from './foreach.js';
22
22
  export * from './workspace.js';
23
23
  export * from './pack.js';
24
24
  export * from './publish.js';
25
+ export * from './release-notes.js';
25
26
  export * from './whoami.js';
26
27
  export * from './login.js';
27
28
  export * from './logout.js';
@@ -22,6 +22,7 @@ export * from './foreach.js';
22
22
  export * from './workspace.js';
23
23
  export * from './pack.js';
24
24
  export * from './publish.js';
25
+ export * from './release-notes.js';
25
26
  export * from './whoami.js';
26
27
  export * from './login.js';
27
28
  export * from './logout.js';
@@ -0,0 +1,7 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface ReleaseNotesOptions {
3
+ release?: string;
4
+ compare?: boolean;
5
+ }
6
+ export declare const releaseNotesCommand: Command<unknown, ReleaseNotesOptions>;
7
+ export {};
@@ -0,0 +1,42 @@
1
+ // `gjsify release-notes [release] [--compare]`
2
+ //
3
+ // Prints one `releases` entry as GitHub Markdown, so a release workflow can pass
4
+ // it to `gh release create --notes-file -` instead of an empty body. Exits 1 when
5
+ // the version has no entry or no description: a release job must fail loudly
6
+ // rather than publish nothing.
7
+ import { Config } from '../config.js';
8
+ import { readPackageJson } from '../utils/pkg-json.js';
9
+ import { renderReleaseNotes, stripVersionPrefix } from '../utils/release-notes.js';
10
+ import { join } from 'node:path';
11
+ export const releaseNotesCommand = {
12
+ command: 'release-notes [release]',
13
+ description: 'Print the `releases` entry for a version as GitHub Markdown. Fails when there is no entry or no description.',
14
+ builder: (yargs) => yargs
15
+ .positional('release', {
16
+ description: 'Version or tag (a leading `v` is accepted). Default: the version in package.json.',
17
+ type: 'string',
18
+ })
19
+ .option('compare', {
20
+ description: 'Append a `**Full changelog:**` compare link to the previous entry (needs `vcsBrowserUrl`).',
21
+ type: 'boolean',
22
+ default: false,
23
+ }),
24
+ handler: async (args) => {
25
+ const projectDir = process.cwd();
26
+ try {
27
+ const config = await new Config().forCommand(projectDir);
28
+ const ship = config.ship ?? {};
29
+ const flatpak = config.flatpak ?? {};
30
+ const version = args.release ?? readPackageJson(join(projectDir, 'package.json'))?.version;
31
+ if (!version)
32
+ throw new Error('no version given and package.json has none');
33
+ const releases = ship.releases ?? flatpak.releases;
34
+ const repoUrl = args.compare === true ? (ship.vcsBrowserUrl ?? flatpak.vcsBrowserUrl) : undefined;
35
+ process.stdout.write(renderReleaseNotes({ releases, version: stripVersionPrefix(version), repoUrl }));
36
+ }
37
+ catch (err) {
38
+ process.stderr.write(`gjsify release-notes: ${err instanceof Error ? err.message : String(err)}\n`);
39
+ return process.exit(1);
40
+ }
41
+ },
42
+ };
@@ -1,4 +1,4 @@
1
- import type { AppMetadata } from '../types/config-data.js';
1
+ import type { AppMetadata, DescriptionBlock } from '../types/config-data.js';
2
2
  /** Everything a renderer needs that is not part of {@link AppMetadata} itself. */
3
3
  export interface AppMetadataInputs {
4
4
  /** Reverse-DNS application id — the MetaInfo `<id>` and the icon/desktop basename. */
@@ -50,4 +50,6 @@ export declare function renderMetainfoCli(inputs: AppMetadataInputs): string;
50
50
  * wins over file types: a URL handler must take `%u`.
51
51
  */
52
52
  export declare function renderDesktopEntry(inputs: AppMetadataInputs): string;
53
+ /** Auto-convert blank-line-split string into paragraph blocks. */
54
+ export declare function stringToBlocks(s: string): DescriptionBlock[];
53
55
  export declare function escapeXml(value: string): string;
@@ -332,7 +332,7 @@ function renderDescriptionBlocks(description, indent) {
332
332
  return out;
333
333
  }
334
334
  /** Auto-convert blank-line-split string into paragraph blocks. */
335
- function stringToBlocks(s) {
335
+ export function stringToBlocks(s) {
336
336
  return s
337
337
  .trim()
338
338
  .split(/\n\n+/)
@@ -281,6 +281,7 @@ export const PACKAGE_DEPS = {
281
281
  '@gjsify/gamepad': ['manette'],
282
282
  '@gjsify/speech-synthesis': ['spiel'],
283
283
  '@gjsify/webaudio': ['gstreamer', 'gst-app'],
284
+ '@gjsify/media-capture': ['gstreamer', 'gst-app'],
284
285
  '@gjsify/iframe': ['webkitgtk'],
285
286
  '@gjsify/canvas2d': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
286
287
  '@gjsify/canvas2d-core': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
@@ -0,0 +1,19 @@
1
+ import type { DescriptionBlock } from '../types/config-data.js';
2
+ export interface ReleaseEntry {
3
+ version: string;
4
+ date: string;
5
+ description?: string | DescriptionBlock[];
6
+ }
7
+ /** Paragraphs → paragraphs, lists → `- ` items. Translator hints are dropped. */
8
+ export declare function descriptionToMarkdown(description: string | DescriptionBlock[]): string;
9
+ /** `v0.1.7` and `0.1.7` name the same release. */
10
+ export declare function stripVersionPrefix(version: string): string;
11
+ export interface ReleaseNotesInput {
12
+ releases: readonly ReleaseEntry[] | undefined;
13
+ /** Requested version, with or without a leading `v`. */
14
+ version: string;
15
+ /** Append the compare link when set and a previous release exists. */
16
+ repoUrl?: string;
17
+ }
18
+ /** Throws an `Error` whose message is meant to be printed as-is. */
19
+ export declare function renderReleaseNotes({ releases, version, repoUrl }: ReleaseNotesInput): string;
@@ -0,0 +1,49 @@
1
+ // GitHub-Markdown rendering of one `releases` entry, for `gjsify release-notes`.
2
+ //
3
+ // The AppStream `<release>` blocks and a GitHub release body describe the same
4
+ // release, so both read the one `releases` list in the config. The string →
5
+ // block normalisation is `stringToBlocks` from app-metadata.ts, not a second copy.
6
+ import { stringToBlocks } from './app-metadata.js';
7
+ /** Paragraphs → paragraphs, lists → `- ` items. Translator hints are dropped. */
8
+ export function descriptionToMarkdown(description) {
9
+ const blocks = typeof description === 'string' ? stringToBlocks(description) : description;
10
+ const out = [];
11
+ for (const block of blocks) {
12
+ if ('p' in block) {
13
+ const text = block.p.trim().replace(/\s+/g, ' ');
14
+ if (text)
15
+ out.push(text);
16
+ }
17
+ else {
18
+ const items = block.ul.map((item) => (typeof item === 'string' ? item : item.item).trim()).filter(Boolean);
19
+ if (items.length)
20
+ out.push(items.map((item) => `- ${item}`).join('\n'));
21
+ }
22
+ }
23
+ return out.join('\n\n');
24
+ }
25
+ /** `v0.1.7` and `0.1.7` name the same release. */
26
+ export function stripVersionPrefix(version) {
27
+ return version.trim().replace(/^v/i, '');
28
+ }
29
+ /** Throws an `Error` whose message is meant to be printed as-is. */
30
+ export function renderReleaseNotes({ releases, version, repoUrl }) {
31
+ const wanted = stripVersionPrefix(version);
32
+ const list = releases ?? [];
33
+ const index = list.findIndex((r) => stripVersionPrefix(r.version) === wanted);
34
+ if (index < 0) {
35
+ throw new Error(`no \`releases\` entry for version ${wanted} in the gjsify config` +
36
+ (list.length ? ` (found: ${list.map((r) => r.version).join(', ')})` : ' (no `releases` configured)'));
37
+ }
38
+ const entry = list[index];
39
+ const body = entry.description === undefined ? '' : descriptionToMarkdown(entry.description);
40
+ if (!body)
41
+ throw new Error(`the \`releases\` entry for version ${wanted} has no description`);
42
+ // Most recent first, so the previous release is the next entry.
43
+ const previous = list[index + 1];
44
+ if (!repoUrl || !previous)
45
+ return `${body}\n`;
46
+ const base = repoUrl.replace(/\/+$/, '');
47
+ const compare = `${base}/compare/v${stripVersionPrefix(previous.version)}...v${wanted}`;
48
+ return `${body}\n\n**Full changelog:** ${compare}\n`;
49
+ }
@@ -170,6 +170,13 @@ export interface Layout {
170
170
  * first line is `import Gtk from 'gi://Gtk?version=4.0'`.
171
171
  */
172
172
  shippedRuntime: 'gjs' | 'node';
173
+ /**
174
+ * Set when the artifact's container stores names in a DECOMPOSED form and a
175
+ * seal over those names is byte-exact: every path BELOW the layout root is
176
+ * then staged in that form, so the bytes the seal was made over are the
177
+ * bytes the container hands back. See {@link nameFormBelowRoot}.
178
+ */
179
+ nameForm?: 'NFD';
173
180
  /**
174
181
  * Why the staged launcher cannot name {@link shippedRuntime} yet, or absent
175
182
  * when it already does.
@@ -402,6 +409,17 @@ export declare function assertLayoutSupportsArch(layout: Layout, arch: string):
402
409
  * `lib/<name>/`, because on macOS those two go to different directories.
403
410
  */
404
411
  export declare function place(layout: Layout, identity: LayoutIdentity, rel: string): string;
412
+ /**
413
+ * A staged path with every segment BELOW the layout root put in the layout's
414
+ * {@link Layout.nameForm}; the first segment (`<App>.app`) is left as given.
415
+ *
416
+ * The root is exempt for two reasons: no seal covers its own name, and the
417
+ * packers look the directory up by `${name}.app` on a Linux host whose
418
+ * filesystem compares bytes. Only ever changes non-ASCII names — an ASCII
419
+ * path is returned unchanged, so every bundle that already verified keeps
420
+ * its exact bytes.
421
+ */
422
+ export declare function nameFormBelowRoot(layout: Layout, path: string): string;
405
423
  /**
406
424
  * Apply a layout to a whole planned payload, and add what the layout itself owns.
407
425
  *
@@ -268,6 +268,14 @@ export const LAYOUTS = {
268
268
  'with `PangoCairo.FontMap.get_default().list_families()`, and `PANGOCAIRO_BACKEND=bogus` to ' +
269
269
  'print which backends it was built with.',
270
270
  launcherExt: '',
271
+ // `codesign` seals the bytes of every name inside the bundle, and the `.dmg` is an
272
+ // HFS+ volume (`dmg.ts`), which stores names decomposed. Measured on macOS 27
273
+ // with an app whose display name holds "ä": the bundle sealed with the precomposed
274
+ // `Contents/Helpers/<name>` verifies out of the zip and fails inside the mounted
275
+ // image ("file added / file missing" on that one path), because HFS+ rewrote the
276
+ // name to a + U+0308. APFS keeps the bytes it is given, so staging NFD makes
277
+ // zip and image agree.
278
+ nameForm: 'NFD',
271
279
  root: appBundleDir,
272
280
  // Apple's, all four. `Contents/MacOS` holds executables, `Contents/Resources`
273
281
  // everything that is not, and `Contents/Frameworks` the dylibs — which is
@@ -680,6 +688,26 @@ export function place(layout, identity, rel) {
680
688
  return posix.join(dirs.data, rel.slice('share/'.length));
681
689
  return posix.join(dirs.other, rel);
682
690
  }
691
+ /**
692
+ * A staged path with every segment BELOW the layout root put in the layout's
693
+ * {@link Layout.nameForm}; the first segment (`<App>.app`) is left as given.
694
+ *
695
+ * The root is exempt for two reasons: no seal covers its own name, and the
696
+ * packers look the directory up by `${name}.app` on a Linux host whose
697
+ * filesystem compares bytes. Only ever changes non-ASCII names — an ASCII
698
+ * path is returned unchanged, so every bundle that already verified keeps
699
+ * its exact bytes.
700
+ */
701
+ export function nameFormBelowRoot(layout, path) {
702
+ if (layout.nameForm === undefined)
703
+ return path;
704
+ const form = layout.nameForm;
705
+ // Staged paths are stage-relative and built with `posix.join` on every host.
706
+ const [root, ...below] = path.split(posix.sep);
707
+ if (below.length === 0)
708
+ return path;
709
+ return [root, ...below.map((segment) => segment.normalize(form))].join(posix.sep);
710
+ }
683
711
  /**
684
712
  * Apply a layout to a whole planned payload, and add what the layout itself owns.
685
713
  *
@@ -712,7 +740,8 @@ export function place(layout, identity, rel) {
712
740
  export function placeStage(layout, identity, files, carried = []) {
713
741
  const byPlaced = new Map();
714
742
  const out = [];
715
- const claim = (path, from, file) => {
743
+ const claim = (rawPath, from, file) => {
744
+ const path = nameFormBelowRoot(layout, rawPath);
716
745
  const previous = byPlaced.get(path);
717
746
  if (previous !== undefined) {
718
747
  throw new Error(`gjsify ship: in the ${layout.name} layout both ${previous} and ${from} install as ` +
@@ -66,6 +66,31 @@ export interface SignerDescriptor {
66
66
  /** What a signature from this signer does NOT yet cover — printed once per run. */
67
67
  gap: string;
68
68
  }
69
+ /**
70
+ * An explicit `--identifier` for an image whose FILE NAME is not ASCII, or
71
+ * `undefined` when codesign's own default is already right.
72
+ *
73
+ * WHY THIS EXISTS, measured on macOS 2026-10-30 (steuererklaerung CI run
74
+ * 41050980433, plus mini bundles beside it): the darwin layout puts the
75
+ * interpreter at `Contents/Helpers/<display name>`, so an app called
76
+ * "Steuererklärung" carries a non-ASCII leaf. codesign derives an image's code
77
+ * identifier from that file name; with a NAMED identity the designated
78
+ * requirement becomes `identifier "<derived>" and certificate leaf …`, and
79
+ * `codesign --verify --strict <App>.app` then fails with *"the sealed resource
80
+ * directory is invalid"* for the NFD-derived identifier (`…61cc88…`); why
81
+ * codesign rejects it is not established, only that it does. Ad-hoc (a cdhash
82
+ * requirement, no identifier at all) and ASCII names are unaffected; the same
83
+ * bundle with `--identifier eu.test.x.helper` verifies, and an NFC identifier
84
+ * still does not, so the value has to be plain ASCII rather than merely
85
+ * normalised.
86
+ *
87
+ * Hence: an ASCII leaf keeps codesign's default (this returns `undefined` and
88
+ * the flag is not sent, so every name that already worked is unchanged), and
89
+ * only a non-ASCII leaf gets a stable ASCII identifier. NFD first, so a
90
+ * precomposed "ä" and a decomposed "a"+U+0308 fold to the same value instead of
91
+ * to two identifiers that differ by bytes nobody can see.
92
+ */
93
+ export declare function asciiCodesignIdentifier(fileName: string): string | undefined;
69
94
  /**
70
95
  * The signers, keyed by the LAYOUT they sign — never by the host they run on.
71
96
  *
@@ -156,6 +181,17 @@ export interface SignArgs {
156
181
  entitlements?: string;
157
182
  /** Is {@link identity} the reserved ad-hoc `-`? */
158
183
  adhoc: boolean;
184
+ /**
185
+ * Is {@link file} the bundle DIRECTORY being sealed, rather than one image
186
+ * inside it?
187
+ *
188
+ * A signer needs the distinction because the two invocations are not the
189
+ * same operation on the same kind of object: a bundle takes its identifier
190
+ * from `Info.plist` and must never be given one, while an image's identifier
191
+ * is derived from its own file name — which is where a non-ASCII helper name
192
+ * becomes a problem ({@link asciiCodesignIdentifier}).
193
+ */
194
+ seal: boolean;
159
195
  }
160
196
  /** `codesign --sign -`: the one identity value that names no identity at all. */
161
197
  export declare const ADHOC_IDENTITY = "-";
@@ -74,13 +74,57 @@
74
74
  // ad-hoc answer, and § A4's re-sign of every image stays the design of record for
75
75
  // the Developer ID that notarisation will actually require.
76
76
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
77
- import { dirname, join, sep } from 'node:path';
77
+ import { basename, dirname, join, sep } from 'node:path';
78
78
  import { isOnPath } from '../check-system-deps.js';
79
79
  import { describeExit, spawnToCompletion } from '../spawn.js';
80
80
  import { listFilesRecursive } from './discover.js';
81
81
  import { classifyBinary } from './payload.js';
82
82
  import { renderEntitlements } from './plist.js';
83
83
  import { writePayload } from './stage-writer.js';
84
+ /**
85
+ * An explicit `--identifier` for an image whose FILE NAME is not ASCII, or
86
+ * `undefined` when codesign's own default is already right.
87
+ *
88
+ * WHY THIS EXISTS, measured on macOS 2026-10-30 (steuererklaerung CI run
89
+ * 41050980433, plus mini bundles beside it): the darwin layout puts the
90
+ * interpreter at `Contents/Helpers/<display name>`, so an app called
91
+ * "Steuererklärung" carries a non-ASCII leaf. codesign derives an image's code
92
+ * identifier from that file name; with a NAMED identity the designated
93
+ * requirement becomes `identifier "<derived>" and certificate leaf …`, and
94
+ * `codesign --verify --strict <App>.app` then fails with *"the sealed resource
95
+ * directory is invalid"* for the NFD-derived identifier (`…61cc88…`); why
96
+ * codesign rejects it is not established, only that it does. Ad-hoc (a cdhash
97
+ * requirement, no identifier at all) and ASCII names are unaffected; the same
98
+ * bundle with `--identifier eu.test.x.helper` verifies, and an NFC identifier
99
+ * still does not, so the value has to be plain ASCII rather than merely
100
+ * normalised.
101
+ *
102
+ * Hence: an ASCII leaf keeps codesign's default (this returns `undefined` and
103
+ * the flag is not sent, so every name that already worked is unchanged), and
104
+ * only a non-ASCII leaf gets a stable ASCII identifier. NFD first, so a
105
+ * precomposed "ä" and a decomposed "a"+U+0308 fold to the same value instead of
106
+ * to two identifiers that differ by bytes nobody can see.
107
+ */
108
+ export function asciiCodesignIdentifier(fileName) {
109
+ // `fileName` may be a path, and codesign derives the identifier from the
110
+ // LEAF — so both the ASCII test and the fallback bytes are about the leaf.
111
+ const leaf = basename(fileName);
112
+ if ([...leaf].every((char) => (char.codePointAt(0) ?? 0) < 0x80))
113
+ return undefined;
114
+ const slug = leaf
115
+ .normalize('NFD')
116
+ .replace(/\p{M}+/gu, '')
117
+ .replace(/[^A-Za-z0-9.-]+/g, '-')
118
+ .replace(/^-+|-+$/g, '');
119
+ if (/[A-Za-z0-9]/.test(slug))
120
+ return slug;
121
+ // Nothing Latin survives (a name that is entirely CJK or emoji): a readable
122
+ // slug would be the empty string, which is not an identifier, so the UTF-8
123
+ // bytes are spelled out — ASCII, unique, and reproducible from the name it
124
+ // came from.
125
+ const bytes = new TextEncoder().encode(leaf);
126
+ return `x-${[...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('')}`;
127
+ }
84
128
  /**
85
129
  * The signers, keyed by the LAYOUT they sign — never by the host they run on.
86
130
  *
@@ -116,16 +160,25 @@ export const SIGNERS = {
116
160
  // the timestamp authority service cannot be contacted … the signing
117
161
  // operation will fail", which is not a failure worth inventing for a
118
162
  // signature that cannot carry the result.
119
- args: ({ identity, file, entitlements, adhoc }) => [
120
- '--force',
121
- '--sign',
122
- identity,
123
- '--options',
124
- 'runtime',
125
- ...(entitlements === undefined ? [] : ['--entitlements', entitlements]),
126
- ...(adhoc ? [] : ['--timestamp']),
127
- file,
128
- ],
163
+ args: ({ identity, file, entitlements, adhoc, seal }) => {
164
+ // A non-ASCII leaf gets an explicit ASCII identifier — see
165
+ // {@link asciiCodesignIdentifier}. NEVER for the seal: the bundle
166
+ // takes its identifier from `CFBundleIdentifier` in `Info.plist`,
167
+ // and overriding that would make the seal disagree with the bundle
168
+ // the system reads.
169
+ const derived = seal ? undefined : asciiCodesignIdentifier(file);
170
+ return [
171
+ '--force',
172
+ '--sign',
173
+ identity,
174
+ '--options',
175
+ 'runtime',
176
+ ...(entitlements === undefined ? [] : ['--entitlements', entitlements]),
177
+ ...(adhoc ? [] : ['--timestamp']),
178
+ ...(derived === undefined ? [] : ['--identifier', derived]),
179
+ file,
180
+ ];
181
+ },
129
182
  // FIVE OF THE REFERENCE'S SIX for an ad-hoc identity, FOUR for a real one,
130
183
  // and the difference is the one § A16 left open — now MEASURED, on
131
184
  // darwin-arm64 with the published `@gjsify/gtk-runtime-darwin-arm64` staged
@@ -182,7 +235,9 @@ export const SIGNERS = {
182
235
  gap: 'the images are signed and the bundle is sealed, and the seal survives both the `.zip` and the ' +
183
236
  '`.dmg` round trip; § A16 stands measured in ONE direction only — library validation refuses an ' +
184
237
  'ad-hoc-signed dylib, so the ad-hoc identity grants `disable-library-validation` while a named ' +
185
- 'one does not, and no run in this repository has ever held the named one',
238
+ 'one does not; a named Developer ID was signed and notarised on 2026-10-30 for an ASCII-named ' +
239
+ 'app, and a non-ASCII image name verifies only with the explicit ASCII `--identifier` this ' +
240
+ 'signer now passes',
186
241
  },
187
242
  win32: {
188
243
  layoutOs: 'win32',
@@ -335,8 +390,14 @@ export async function signPayload(input) {
335
390
  writeFileSync(input.entitlementsPath, renderEntitlements(granted));
336
391
  entitlements = input.entitlementsPath;
337
392
  }
338
- const run = async (file, what) => {
339
- const args = signer.args({ identity: input.identity, file, adhoc, ...(entitlements ? { entitlements } : {}) });
393
+ const run = async (file, what, seal) => {
394
+ const args = signer.args({
395
+ identity: input.identity,
396
+ file,
397
+ adhoc,
398
+ seal,
399
+ ...(entitlements ? { entitlements } : {}),
400
+ });
340
401
  if (input.verbose)
341
402
  input.log(`${signer.tool} ${args.join(' ')}`);
342
403
  const result = await spawnToCompletion(signer.tool, args, {
@@ -367,7 +428,7 @@ export async function signPayload(input) {
367
428
  return [...payload];
368
429
  }
369
430
  for (const entry of signable) {
370
- await run(join(workDir, entry.path.split('/').join(sep)), entry.path);
431
+ await run(join(workDir, entry.path.split('/').join(sep)), entry.path, false);
371
432
  }
372
433
  // THE IMAGES FIRST, THE SEAL LAST, and the order is not stylistic: a bundle
373
434
  // seal hashes the files it seals, so sealing before the closure is signed
@@ -381,7 +442,7 @@ export async function signPayload(input) {
381
442
  'stage would sign the payload of every other format beside it.');
382
443
  }
383
444
  sealPrefix = signer.sealAddPrefix(input.bundleRoot);
384
- await run(join(workDir, input.bundleRoot.split('/').join(sep)), `${input.bundleRoot} (bundle seal)`);
445
+ await run(join(workDir, input.bundleRoot.split('/').join(sep)), `${input.bundleRoot} (bundle seal)`, true);
385
446
  }
386
447
  // THE FILE SET MAY ONLY GROW WHERE THE SEAL WRITES. Both tools rewrite an
387
448
  // image in place, so any other new path means something else ran — and a file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.64.13-next.283",
3
+ "version": "0.64.14",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -142,22 +142,22 @@
142
142
  "cli"
143
143
  ],
144
144
  "dependencies": {
145
- "@gjsify/blueprint": "0.64.13-next.283",
146
- "@gjsify/buffer": "0.64.13-next.283",
147
- "@gjsify/create-app": "0.64.13-next.283",
148
- "@gjsify/node-globals": "0.64.13-next.283",
149
- "@gjsify/node-polyfills": "0.64.13-next.283",
150
- "@gjsify/npm-registry": "0.64.13-next.283",
151
- "@gjsify/resolve-npm": "0.64.13-next.283",
152
- "@gjsify/rolldown-plugin-gjsify": "0.64.13-next.283",
153
- "@gjsify/vite-plugin-blueprint": "0.64.13-next.283",
154
- "@gjsify/rolldown-plugin-pnp": "0.64.13-next.283",
155
- "@gjsify/semver": "0.64.13-next.283",
156
- "@gjsify/tar": "0.64.13-next.283",
157
- "@gjsify/tsc": "0.64.13-next.283",
158
- "@gjsify/utils": "0.64.13-next.283",
159
- "@gjsify/web-polyfills": "0.64.13-next.283",
160
- "@gjsify/workspace": "0.64.13-next.283",
145
+ "@gjsify/blueprint": "^0.64.14",
146
+ "@gjsify/buffer": "^0.64.14",
147
+ "@gjsify/create-app": "^0.64.14",
148
+ "@gjsify/node-globals": "^0.64.14",
149
+ "@gjsify/node-polyfills": "^0.64.14",
150
+ "@gjsify/npm-registry": "^0.64.14",
151
+ "@gjsify/resolve-npm": "^0.64.14",
152
+ "@gjsify/rolldown-plugin-gjsify": "^0.64.14",
153
+ "@gjsify/vite-plugin-blueprint": "^0.64.14",
154
+ "@gjsify/rolldown-plugin-pnp": "^0.64.14",
155
+ "@gjsify/semver": "^0.64.14",
156
+ "@gjsify/tar": "^0.64.14",
157
+ "@gjsify/tsc": "^0.64.14",
158
+ "@gjsify/utils": "^0.64.14",
159
+ "@gjsify/web-polyfills": "^0.64.14",
160
+ "@gjsify/workspace": "^0.64.14",
161
161
  "acorn": "^8.17.0",
162
162
  "cosmiconfig": "^9.0.2",
163
163
  "get-tsconfig": "^4.14.0",
@@ -166,13 +166,13 @@
166
166
  "yargs": "^18.0.0"
167
167
  },
168
168
  "devDependencies": {
169
- "@gjsify/unit": "0.64.13-next.283",
169
+ "@gjsify/unit": "^0.64.14",
170
170
  "@types/yargs": "^17.0.35",
171
171
  "typescript": "^6.0.3"
172
172
  },
173
173
  "peerDependencies": {
174
- "@gjsify/oxfmt-native": "0.64.13-next.283",
175
- "@gjsify/rolldown-native": "0.64.13-next.283"
174
+ "@gjsify/oxfmt-native": "^0.64.14",
175
+ "@gjsify/rolldown-native": "^0.64.14"
176
176
  },
177
177
  "peerDependenciesMeta": {
178
178
  "@gjsify/oxfmt-native": {
@@ -191,6 +191,5 @@
191
191
  "bugs": {
192
192
  "url": "https://github.com/gjsify/gjsify/issues"
193
193
  },
194
- "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/cli#readme",
195
- "gitHead": "05fae97125a374e84bf57c2d2d086fb53523f205"
194
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/cli#readme"
196
195
  }