dsh-scheduled-message 0.1.1

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/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "dsh-scheduled-message",
3
+ "version": "0.1.1",
4
+ "description": "Schedule the message you are writing to send later, or turn it into a repeating automation task.",
5
+ "license": "MIT",
6
+ "author": "intelfabs",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/intelfabs/dsh-scheduled-message.git"
10
+ },
11
+ "homepage": "https://github.com/intelfabs/dsh-scheduled-message#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/intelfabs/dsh-scheduled-message/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "type": "module",
19
+ "icon": "icon.svg",
20
+ "main": "./index.js",
21
+ "exports": {
22
+ ".": "./index.js",
23
+ "./client": "./client.js",
24
+ "./locale/*.json": "./locale/*.json",
25
+ "./icon": "./icon.svg",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "index.js",
30
+ "client.js",
31
+ "lib",
32
+ "locale",
33
+ "docs",
34
+ "scripts",
35
+ "cordis.patch.yml",
36
+ "icon.svg",
37
+ "LICENSE",
38
+ "README.md"
39
+ ],
40
+ "keywords": [
41
+ "deepseek-harness",
42
+ "dsh-plugin",
43
+ "dsh",
44
+ "cordis",
45
+ "schedule",
46
+ "scheduled-message"
47
+ ],
48
+ "engines": {
49
+ "dsh": ">=0.2.2-rc.2"
50
+ },
51
+ "scripts": {
52
+ "build": "node scripts/build-client.mjs",
53
+ "test": "node --test test/core.test.mjs test/client.test.mjs test/host.test.mjs",
54
+ "check": "node scripts/check.mjs"
55
+ },
56
+ "dsh": {
57
+ "bundle": {
58
+ "patch": "./cordis.patch.yml"
59
+ },
60
+ "client": {
61
+ "platform": "web",
62
+ "immediately": true
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build the browser bundle `client.js` from its three sources.
4
+ *
5
+ * The Client half is one lazily materialized CJS factory, so it cannot import
6
+ * `lib/core.js` or read `locale/*.json` at runtime. This script inlines both
7
+ * into `client.src.js`'s factory: `core` stays the single implementation of the
8
+ * date, 12-hour clock and RRULE logic, and the dictionaries keep one source of
9
+ * truth. `test/client.test.mjs` rebuilds in memory and fails when the committed
10
+ * bundle differs, so the artifact can never drift from its sources.
11
+ *
12
+ * @module dsh-scheduled-message/scripts/build-client
13
+ */
14
+
15
+ import { readFileSync, writeFileSync } from 'node:fs'
16
+ import { dirname, join } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
20
+ const PACKAGE_NAME = 'dsh-scheduled-message'
21
+
22
+ /**
23
+ * Read one package-relative file.
24
+ * @param relative - path below the package root.
25
+ * @returns the file text.
26
+ */
27
+ function read(relative) {
28
+ return readFileSync(join(ROOT, relative), 'utf8')
29
+ }
30
+
31
+ /**
32
+ * Turn `lib/core.js` into a factory-local module object.
33
+ * @returns source for one `const core = (() => { … })()` expression.
34
+ */
35
+ function coreBlock() {
36
+ const source = read('lib/core.js')
37
+ const names = [...source.matchAll(/^export (?:const|function|class)\s+([A-Za-z_$][\w$]*)/gm)].map(match => match[1])
38
+ if (names.length === 0) throw new Error('lib/core.js exports nothing to inline')
39
+ if (/^export\s+(?!const|function|class)/m.test(source)) {
40
+ throw new Error('lib/core.js uses an export form the bundler cannot inline')
41
+ }
42
+ const body = source.replace(/^export\s+/gm, '')
43
+ return `const core = (() => {\n${body}\nreturn { ${names.join(', ')} }\n})()`
44
+ }
45
+
46
+ /**
47
+ * Turn both dictionaries into factory-local constants.
48
+ * @returns source declaring `EN` and `ZH`.
49
+ */
50
+ function localeBlock() {
51
+ const en = JSON.parse(read('locale/en.json'))
52
+ const zh = JSON.parse(read('locale/zh.json'))
53
+ const missing = Object.keys(en).filter(key => !(key in zh))
54
+ const extra = Object.keys(zh).filter(key => !(key in en))
55
+ if (missing.length > 0 || extra.length > 0) {
56
+ throw new Error(`locale key mismatch: en-only ${JSON.stringify(missing)}, zh-only ${JSON.stringify(extra)}`)
57
+ }
58
+ return `const EN = ${JSON.stringify(en, null, 2)}\n\nconst ZH = ${JSON.stringify(zh, null, 2)}`
59
+ }
60
+
61
+ /**
62
+ * Compose the whole bundle text.
63
+ * @returns the `client.js` artifact.
64
+ */
65
+ export function buildClient() {
66
+ const source = read('client.src.js')
67
+ if (!/^export function createClientPlugin\(require\) \{/m.test(source)) {
68
+ throw new Error('client.src.js must export exactly one createClientPlugin factory')
69
+ }
70
+ const plugin = source.replace(/^export function createClientPlugin\(require\) \{/m, 'function createClientPlugin(require) {')
71
+ return [
72
+ '/**',
73
+ ' * dsh-scheduled-message — browser bundle. GENERATED FILE, do not edit.',
74
+ ' *',
75
+ ' * Built from client.src.js + lib/core.js + locale/en.json + locale/zh.json by',
76
+ ' * scripts/build-client.mjs. Rebuild with `node scripts/build-client.mjs`;',
77
+ ' * `node --test test/` fails when this file is stale.',
78
+ ' */',
79
+ 'window.__ModuleLoader__.load({',
80
+ ` id: ${JSON.stringify(PACKAGE_NAME)},`,
81
+ ' factory: (require) => {',
82
+ coreBlock(),
83
+ '',
84
+ localeBlock(),
85
+ '',
86
+ plugin,
87
+ '',
88
+ ' return createClientPlugin(require)',
89
+ ' },',
90
+ '})',
91
+ '',
92
+ ].join('\n')
93
+ }
94
+
95
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
96
+ const target = join(ROOT, 'client.js')
97
+ writeFileSync(target, buildClient(), 'utf8')
98
+ process.stdout.write(`wrote ${target}\n`)
99
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Build the browser bundle and verify the package is internally consistent.
3
+ *
4
+ * Static gates only: nothing here imports the Host half, so a missing DSH
5
+ * installation cannot make the check fail for the wrong reason.
6
+ * @module dsh-scheduled-message/scripts/check
7
+ */
8
+
9
+ import { execFileSync } from 'node:child_process'
10
+ import { readFileSync, readdirSync } from 'node:fs'
11
+ import { dirname, join } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { buildClient } from './build-client.mjs'
14
+
15
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
16
+ const failures = []
17
+
18
+ /**
19
+ * Assert one condition, collecting a readable failure instead of throwing.
20
+ * @param condition - the condition to require.
21
+ * @param message - diagnostic for a failure.
22
+ */
23
+ function requireThat(condition, message) {
24
+ if (!condition) failures.push(message)
25
+ }
26
+
27
+ // 1. The committed bundle must equal a fresh build.
28
+ const expected = buildClient()
29
+ const actual = readFileSync(join(ROOT, 'client.js'), 'utf8')
30
+ requireThat(actual === expected, 'client.js is stale: run `node scripts/build-client.mjs`')
31
+
32
+ // 2. Both halves and the tests must parse.
33
+ for (const file of ['index.js', 'lib/core.js', 'client.js', 'client.src.js']) {
34
+ try {
35
+ execFileSync(process.execPath, ['--check', join(ROOT, file)], { stdio: 'pipe' })
36
+ } catch (error) {
37
+ failures.push(`${file} does not parse: ${String(error.stderr ?? error.message).split('\n')[0]}`)
38
+ }
39
+ }
40
+
41
+ // 3. package.json and both dictionaries must parse and agree on their keys.
42
+ const manifest = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
43
+ requireThat(manifest.type === 'module', 'package.json must declare "type": "module"')
44
+ requireThat(manifest.main === './index.js', 'package.json main must be ./index.js')
45
+ requireThat(manifest.exports?.['./client'] === './client.js', 'package.json must export ./client -> ./client.js')
46
+ requireThat(manifest.dsh?.client?.platform === 'web', 'package.json must declare dsh.client.platform = web')
47
+ requireThat(manifest.dsh?.bundle?.patch === './cordis.patch.yml', 'package.json must point dsh.bundle.patch at cordis.patch.yml')
48
+
49
+ const en = JSON.parse(readFileSync(join(ROOT, 'locale/en.json'), 'utf8'))
50
+ const zh = JSON.parse(readFileSync(join(ROOT, 'locale/zh.json'), 'utf8'))
51
+ requireThat(Object.keys(en).length > 0, 'locale/en.json is empty')
52
+ requireThat(
53
+ JSON.stringify(Object.keys(en).sort()) === JSON.stringify(Object.keys(zh).sort()),
54
+ 'locale/en.json and locale/zh.json must declare the same keys',
55
+ )
56
+
57
+ // 4. Every key the bundle asks for must exist in the dictionary. Keys composed
58
+ // at runtime (repeat.<freq>, repeat.detail.<freq>, weekday.<day>, error.<code>,
59
+ // attachments.<form>) are covered by the parity check below and by the smoke
60
+ // test's rendering paths, so only literal keys are counted here.
61
+ const usedKeys = new Set()
62
+ const build = readFileSync(join(ROOT, 'client.src.js'), 'utf8')
63
+ for (const match of build.matchAll(/\bt\(\s*'([^']+)'/g)) usedKeys.add(match[1])
64
+ for (const match of build.matchAll(/label:\s*'([a-z][A-Za-z0-9.]*)'/g)) usedKeys.add(match[1])
65
+ for (const key of usedKeys) {
66
+ requireThat(key in en, `client.src.js uses t('${key}') but locale/en.json does not define it`)
67
+ requireThat(key in zh, `client.src.js uses t('${key}') but locale/zh.json does not define it`)
68
+ }
69
+
70
+ // 5. Files the manifest promises must exist.
71
+ for (const file of ['index.js', 'client.js', 'lib/core.js', 'icon.svg', 'cordis.patch.yml', 'README.md', 'docs/DETAIL.md']) {
72
+ try {
73
+ readFileSync(join(ROOT, file))
74
+ } catch {
75
+ failures.push(`missing file: ${file}`)
76
+ }
77
+ }
78
+
79
+ // 6. The bundle must not require anything outside the platform table.
80
+ for (const match of expected.matchAll(/require\(\s*'([^']+)'\s*\)/g)) {
81
+ requireThat(
82
+ ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'].includes(match[1]),
83
+ `client.js requires '${match[1]}', which is not in the browser platform table`,
84
+ )
85
+ }
86
+
87
+ // 7. The package must ship no TypeScript-only imports in the shipped halves.
88
+ for (const file of readdirSync(join(ROOT, 'lib')).filter(name => name.endsWith('.js'))) {
89
+ const source = readFileSync(join(ROOT, 'lib', file), 'utf8')
90
+ requireThat(!/^import\s/m.test(source), `lib/${file} must stay dependency-free`)
91
+ }
92
+
93
+ if (failures.length > 0) {
94
+ process.stderr.write(`check failed:\n${failures.map(line => ` - ${line}`).join('\n')}\n`)
95
+ process.exitCode = 1
96
+ } else {
97
+ process.stdout.write(
98
+ `check passed: ${usedKeys.size} literal locale keys of ${Object.keys(en).length} defined, `
99
+ + `${expected.length} bundle bytes\n`,
100
+ )
101
+ }