create-promptex-js 1.0.0 → 1.1.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.
Files changed (32) hide show
  1. package/README.md +102 -18
  2. package/dist/contract.generated.d.ts +24 -0
  3. package/dist/contract.generated.js +59 -0
  4. package/dist/index.js +356 -176
  5. package/dist/messages.generated.js +145 -61
  6. package/dist/plan.d.ts +105 -0
  7. package/dist/plan.js +327 -0
  8. package/package.json +3 -2
  9. package/templates/adapter-starter/README.md +45 -0
  10. package/templates/adapter-starter/_gitignore +6 -0
  11. package/templates/adapter-starter/example/promptex.config.ts +12 -0
  12. package/templates/adapter-starter/example/prompts/nodes.ts +18 -0
  13. package/templates/adapter-starter/package.json +28 -0
  14. package/templates/adapter-starter/promptex.config.schema.json +12 -0
  15. package/templates/adapter-starter/src/index.ts +93 -0
  16. package/templates/adapter-starter/tsconfig.json +14 -0
  17. package/templates/plugin-starter/README.md +40 -0
  18. package/templates/plugin-starter/_gitignore +5 -0
  19. package/templates/plugin-starter/example/promptex.config.ts +12 -0
  20. package/templates/plugin-starter/example/prompts/nodes.ts +16 -0
  21. package/templates/plugin-starter/package.json +28 -0
  22. package/templates/plugin-starter/promptex.config.schema.json +12 -0
  23. package/templates/plugin-starter/src/index.ts +63 -0
  24. package/templates/plugin-starter/tsconfig.json +14 -0
  25. package/templates/project-starter/README.md +30 -0
  26. package/templates/project-starter/_gitignore +11 -0
  27. package/templates/project-starter/package.json +13 -0
  28. package/templates/project-starter/promptex.config.ts +8 -0
  29. package/templates/project-starter/prompts/example-skill.ts +14 -0
  30. package/templates/project-starter/prompts/example.ts +14 -0
  31. package/dist/default-ref.d.ts +0 -6
  32. package/dist/default-ref.js +0 -14
package/dist/plan.js ADDED
@@ -0,0 +1,327 @@
1
+ // create 的模式判斷、寫入規則與套用(m007 d004)。三個 create 套件各有一份同構的實作:
2
+ // create-promptex-py 的 `_plan.py` 與 create-promptex-rs 的 `plan.rs`。函式名、規則表與
3
+ // 輸出欄位三份對齊,允許清單與錯誤代碼則由 `contract.generated.ts` 從同一份來源生成。
4
+ //
5
+ // 流程分四段:inspectTarget(目標狀態與模式)→ planFiles(逐檔決定動作,不寫入)→
6
+ // applyPlan(照計畫寫入,失敗時只回復這次新建的東西)→ 由 CLI 輸出報告或 JSON。
7
+ // --dry-run 只走前兩段。
8
+ import { spawnSync } from 'node:child_process';
9
+ import { appendFileSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
10
+ import { dirname, join, relative, resolve, isAbsolute } from 'node:path';
11
+ import { CONFIG_FILES, GITIGNORE_MARKER, isAllowlisted } from './contract.generated.js';
12
+ /** 依代碼帶結構化細節的失敗;CLI 依 `contract.generated.ts` 的錯誤表決定訊息與結束碼。 */
13
+ export class CreateError extends Error {
14
+ code;
15
+ details;
16
+ constructor(code, details = {}) {
17
+ super(code);
18
+ this.code = code;
19
+ this.details = details;
20
+ }
21
+ }
22
+ function lstatOrNull(path) {
23
+ return lstatSync(path, { throwIfNoEntry: false }) ?? null;
24
+ }
25
+ /**
26
+ * 判斷目標狀態與模式。目標不存在,或第一層只有允許清單裡的項目,就走建立模式。
27
+ * 目標是符號連結或檔案時以用法錯誤拒絕,不寫入任何東西。
28
+ */
29
+ export function inspectTarget(target) {
30
+ const stat = lstatOrNull(target);
31
+ if (stat === null)
32
+ return { exists: false, mode: 'create', unlisted: [] };
33
+ if (stat.isSymbolicLink())
34
+ throw new CreateError('USAGE_TARGET_IS_SYMLINK', { path: target });
35
+ if (!stat.isDirectory())
36
+ throw new CreateError('USAGE_TARGET_IS_FILE', { path: target });
37
+ const unlisted = readdirSync(target)
38
+ .filter((name) => !isAllowlisted(name))
39
+ .sort();
40
+ return { exists: true, mode: unlisted.length === 0 ? 'create' : 'init', unlisted };
41
+ }
42
+ /** 目標裡已存在的 promptex 設定檔;有就代表已初始化。 */
43
+ export function findConfig(target) {
44
+ return CONFIG_FILES.find((name) => lstatOrNull(join(target, name)) !== null) ?? null;
45
+ }
46
+ /**
47
+ * 目標是否位於任何 git 工作樹內(含上層資料夾)。往上找 `.git`(資料夾或 worktree 的
48
+ * 檔案),不呼叫 git:沒裝 git 也判得出來,三種語言的結果也因此一致。
49
+ */
50
+ export function isInsideGitWorktree(target) {
51
+ let dir = resolve(target);
52
+ while (lstatOrNull(dir) === null && dirname(dir) !== dir)
53
+ dir = dirname(dir);
54
+ for (;;) {
55
+ if (lstatOrNull(join(dir, '.git')) !== null)
56
+ return true;
57
+ const parent = dirname(dir);
58
+ if (parent === dir)
59
+ return false;
60
+ dir = parent;
61
+ }
62
+ }
63
+ export function commandAvailable(name) {
64
+ return spawnSync(name, ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' }).status === 0;
65
+ }
66
+ /** 非互動環境:沒有終端機、帶 --yes,或 CI 環境變數為真(空字串、0、false 不算)。 */
67
+ export function isNonInteractive(stdinIsTty, yes, env) {
68
+ const ci = (env.CI ?? '').trim().toLowerCase();
69
+ return !stdinIsTty || yes || (ci !== '' && ci !== '0' && ci !== 'false');
70
+ }
71
+ /** 非互動時沒給又沒有預設值的輸入,一次全列出(結束碼 2)。互動時會問,因此不算缺。 */
72
+ export function missingInputs(inputs, interactive) {
73
+ if (interactive)
74
+ return [];
75
+ return inputs.filter((input) => !input.provided && !input.hasDefault).map((input) => input.flag);
76
+ }
77
+ /** 宣告檔與範例目錄依範本而定,其他角色看檔名。 */
78
+ const MANIFEST = 'package.json';
79
+ const EXAMPLE_DIR = { project: 'prompts', plugin: 'example', adapter: 'example' };
80
+ /**
81
+ * 每個角色遇到同名既有檔時怎麼做(m007 d004 寫入規則表)。`.gitignore` 走追加,不在表內。
82
+ * `forceOverwrites` 為假的角色連 --force 都不覆寫;`createModeOnly` 的角色在初始化模式一律不寫。
83
+ */
84
+ const RULES = {
85
+ manifest: { existsReason: 'manifest-kept', forceOverwrites: false, createModeOnly: false },
86
+ readme: { existsReason: 'exists', forceOverwrites: true, createModeOnly: true },
87
+ example: { existsReason: 'exists', forceOverwrites: true, createModeOnly: false },
88
+ other: { existsReason: 'exists', forceOverwrites: true, createModeOnly: false },
89
+ };
90
+ export function roleOf(rel, template) {
91
+ if (rel === MANIFEST)
92
+ return 'manifest';
93
+ if (rel === 'README.md')
94
+ return 'readme';
95
+ if (rel === '.gitignore')
96
+ return 'gitignore';
97
+ if (rel.startsWith(`${EXAMPLE_DIR[template]}/`))
98
+ return 'example';
99
+ return 'other';
100
+ }
101
+ /** 上層路徑擋住寫入的原因:符號連結或不是資料夾。全部是資料夾或不存在時回 null。 */
102
+ function blockedParent(target, rel) {
103
+ const parts = rel.split('/').slice(0, -1);
104
+ let current = target;
105
+ for (const part of parts) {
106
+ current = join(current, part);
107
+ const stat = lstatOrNull(current);
108
+ if (stat === null)
109
+ return null;
110
+ if (stat.isSymbolicLink())
111
+ return 'symlink';
112
+ if (!stat.isDirectory())
113
+ return 'blocked';
114
+ }
115
+ return null;
116
+ }
117
+ /**
118
+ * `.gitignore`:不存在就建立;存在就在尾端追加帶標記的區塊,已有標記行就跳過。
119
+ * 追加的區塊前補換行與一個空行,作者的最後一條規則才不會和標記行黏成同一行
120
+ * (與核心 init 的 `apply_gitignore` 相同)。範本沒有標記行時(plugin、adapter)
121
+ * 追加前補上,重跑才判得出已追加過。
122
+ */
123
+ function planGitignore(target, content) {
124
+ const path = join(target, '.gitignore');
125
+ const stat = lstatOrNull(path);
126
+ if (stat === null)
127
+ return { action: 'create', path: '.gitignore', content };
128
+ if (stat.isSymbolicLink())
129
+ return { action: 'skip', path: '.gitignore', reason: 'symlink' };
130
+ let existing;
131
+ try {
132
+ existing = readFileSync(path, 'utf8');
133
+ }
134
+ catch (e) {
135
+ throw new CreateError('RUN_READ_FAILED', { path, detail: e instanceof Error ? e.message : String(e) });
136
+ }
137
+ if (existing.split(/\r?\n/).includes(GITIGNORE_MARKER)) {
138
+ return { action: 'skip', path: '.gitignore', reason: 'marker-present' };
139
+ }
140
+ const block = content.startsWith(GITIGNORE_MARKER) ? content : `${GITIGNORE_MARKER}\n${content}`;
141
+ let prefix = '';
142
+ if (existing !== '' && !existing.endsWith('\n'))
143
+ prefix += '\n';
144
+ if (existing !== '')
145
+ prefix += '\n';
146
+ return { action: 'append', path: '.gitignore', content: prefix + block };
147
+ }
148
+ /** 逐檔決定動作,不寫入。回傳的順序是範本檔的路徑順序。 */
149
+ export function planFiles(target, files, mode, template, force) {
150
+ const exampleDir = EXAMPLE_DIR[template];
151
+ const exampleSkipped = !force && lstatOrNull(join(target, exampleDir)) !== null;
152
+ const actions = [];
153
+ let exampleListed = false;
154
+ for (const file of [...files].sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0))) {
155
+ const role = roleOf(file.rel, template);
156
+ if (role === 'example' && exampleSkipped) {
157
+ if (!exampleListed)
158
+ actions.push({ action: 'skip', path: `${exampleDir}/`, reason: 'example-dir-exists' });
159
+ exampleListed = true;
160
+ continue;
161
+ }
162
+ const blocked = blockedParent(target, file.rel);
163
+ if (blocked !== null) {
164
+ actions.push({ action: 'skip', path: file.rel, reason: blocked });
165
+ continue;
166
+ }
167
+ if (role === 'gitignore') {
168
+ actions.push(planGitignore(target, file.content));
169
+ continue;
170
+ }
171
+ const rule = RULES[role];
172
+ if (rule.createModeOnly && mode === 'init') {
173
+ actions.push({ action: 'skip', path: file.rel, reason: 'init-mode' });
174
+ continue;
175
+ }
176
+ const stat = lstatOrNull(join(target, file.rel));
177
+ if (stat === null) {
178
+ actions.push({ action: 'create', path: file.rel, content: file.content });
179
+ }
180
+ else if (stat.isDirectory() || !force || !rule.forceOverwrites) {
181
+ actions.push({ action: 'skip', path: file.rel, reason: rule.existsReason });
182
+ }
183
+ else {
184
+ actions.push({ action: 'overwrite', path: file.rel, content: file.content });
185
+ }
186
+ }
187
+ return actions;
188
+ }
189
+ /** 計畫的四類清單,路徑排序後輸出(JSON 與人讀報告共用)。 */
190
+ export function summarize(actions) {
191
+ const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
192
+ const pick = (kind) => actions.filter((a) => a.action === kind).sort(byPath);
193
+ return {
194
+ create: pick('create').map((a) => a.path),
195
+ overwrite: pick('overwrite').map((a) => a.path),
196
+ append: pick('append').map((a) => a.path),
197
+ skip: pick('skip').map((a) => ({ path: a.path, reason: a.reason })),
198
+ };
199
+ }
200
+ /**
201
+ * 照計畫寫入。先建立與覆寫,最後追加 `.gitignore`,追加之前的失敗因此不會動到既有的
202
+ * `.gitignore`。任何一步失敗時,倒序刪掉這次新建的檔案與資料夾(資料夾只在已清空時刪),
203
+ * 既有檔一律不刪,再以 RUN_WRITE_FAILED 回報。
204
+ *
205
+ * 環境變數 PROMPTEX_CREATE_FAIL_ON 指定一個相對路徑時,寫到那個檔就模擬失敗。只給測試
206
+ * 驗證回復用:真實的寫入失敗(磁碟滿、權限)難以在三種語言穩定重現。
207
+ */
208
+ export function applyPlan(target, actions) {
209
+ const created = [];
210
+ const failOn = process.env.PROMPTEX_CREATE_FAIL_ON;
211
+ const ensureDir = (dir) => {
212
+ const missing = [];
213
+ let probe = dir;
214
+ while (lstatOrNull(probe) === null && dirname(probe) !== probe) {
215
+ missing.unshift(probe);
216
+ probe = dirname(probe);
217
+ }
218
+ for (const path of missing) {
219
+ mkdirSync(path);
220
+ created.push({ path, dir: true });
221
+ }
222
+ };
223
+ let current = target;
224
+ try {
225
+ ensureDir(target);
226
+ const ordered = [
227
+ ...actions.filter((a) => a.action === 'create' || a.action === 'overwrite'),
228
+ ...actions.filter((a) => a.action === 'append'),
229
+ ];
230
+ for (const entry of ordered) {
231
+ current = join(target, entry.path);
232
+ if (failOn === entry.path)
233
+ throw new Error(`simulated failure (PROMPTEX_CREATE_FAIL_ON=${failOn})`);
234
+ ensureDir(dirname(current));
235
+ if (entry.action === 'append') {
236
+ appendFileSync(current, entry.content);
237
+ }
238
+ else if (entry.action === 'overwrite') {
239
+ // 先刪再寫:既有的若是符號連結,覆寫的是連結本身,不跟過去寫外部檔。
240
+ unlinkSync(current);
241
+ writeFileSync(current, entry.content, { flag: 'wx' });
242
+ }
243
+ else {
244
+ writeFileSync(current, entry.content, { flag: 'wx' });
245
+ created.push({ path: current, dir: false });
246
+ }
247
+ }
248
+ }
249
+ catch (e) {
250
+ for (const item of [...created].reverse()) {
251
+ try {
252
+ if (item.dir)
253
+ rmdirSync(item.path);
254
+ else
255
+ unlinkSync(item.path);
256
+ }
257
+ catch {
258
+ // 資料夾裡有別人放的東西,或已經不在:留著,不刪既有內容。
259
+ }
260
+ }
261
+ throw new CreateError('RUN_WRITE_FAILED', { path: current, detail: e instanceof Error ? e.message : String(e) });
262
+ }
263
+ }
264
+ const LOCKFILES = [
265
+ ['pnpm-lock.yaml', 'pnpm'],
266
+ ['yarn.lock', 'yarn'],
267
+ ['bun.lock', 'bun'],
268
+ ['bun.lockb', 'bun'],
269
+ ['package-lock.json', 'npm'],
270
+ ];
271
+ export function detectPackageManager(target, pnpmAvailable) {
272
+ let dir = resolve(target);
273
+ for (;;) {
274
+ for (const [file, manager] of LOCKFILES) {
275
+ if (lstatOrNull(join(dir, file)) !== null)
276
+ return manager;
277
+ }
278
+ const parent = dirname(dir);
279
+ if (parent === dir)
280
+ break;
281
+ dir = parent;
282
+ }
283
+ return pnpmAvailable() ? 'pnpm' : 'npm';
284
+ }
285
+ /** 各套件管理器的四種指令形態。 */
286
+ export const PM_COMMANDS = {
287
+ pnpm: { install: 'pnpm install', add: 'pnpm add', run: 'pnpm run', exec: 'pnpm exec' },
288
+ npm: { install: 'npm install', add: 'npm install', run: 'npm run', exec: 'npx' },
289
+ yarn: { install: 'yarn install', add: 'yarn add', run: 'yarn run', exec: 'yarn' },
290
+ bun: { install: 'bun install', add: 'bun add', run: 'bun run', exec: 'bunx' },
291
+ };
292
+ /**
293
+ * 給人看的目標路徑:在工作目錄底下就用相對路徑,目標就是工作目錄時回空字串(不印 `cd .`),
294
+ * 其他情況用絕對路徑。
295
+ */
296
+ export function cdPath(target, cwd) {
297
+ const rel = relative(cwd, target);
298
+ if (rel === '')
299
+ return '';
300
+ if (rel.startsWith('..') || isAbsolute(rel))
301
+ return target;
302
+ return rel.split('\\').join('/');
303
+ }
304
+ export function nextSteps(opts) {
305
+ const pm = PM_COMMANDS[opts.pm];
306
+ const steps = [];
307
+ if (opts.template === 'project') {
308
+ if (opts.cd !== '')
309
+ steps.push(`cd ${opts.cd}`);
310
+ if (opts.manifestKept) {
311
+ // 既有的 package.json 沒有 promptex-js 依賴,也沒有範本的 install:prompts script。
312
+ steps.push(`${pm.add} promptex-js`, `${pm.exec} promptex build --install`);
313
+ }
314
+ else {
315
+ if (!opts.installed)
316
+ steps.push(pm.install);
317
+ steps.push(`${pm.run} install:prompts`);
318
+ }
319
+ return steps;
320
+ }
321
+ if (opts.cd !== '')
322
+ steps.push(`cd ${opts.cd}`);
323
+ if (!opts.installed)
324
+ steps.push(pm.install);
325
+ steps.push(`${pm.run} build`, `cd example && ${pm.exec} promptex build --install`);
326
+ return steps;
327
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-promptex-js",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Scaffold a promptex TypeScript project, plugin or adapter.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "homepage": "https://github.com/promptex-ai/promptex/tree/main/packages/create-promptex-js#readme",
@@ -31,6 +31,7 @@
31
31
  "files": [
32
32
  "bin",
33
33
  "dist",
34
+ "templates",
34
35
  "LICENSE-MIT",
35
36
  "LICENSE-APACHE"
36
37
  ],
@@ -40,7 +41,7 @@
40
41
  "scripts": {
41
42
  "build": "tsc -p tsconfig.json",
42
43
  "pretest": "npm run build",
43
- "test": "node --experimental-strip-types --test test/smoke.test.ts && npm run test:types",
44
+ "test": "node --experimental-strip-types --test test/smoke.test.ts test/modes.test.ts && npm run test:types",
44
45
  "test:types": "tsc -p tsconfig.typelevel.json",
45
46
  "prepack": "npm run build"
46
47
  },
@@ -0,0 +1,45 @@
1
+ # promptex-adapter-{{name}}
2
+
3
+ A publishable third-party [promptex](https://github.com/promptex-ai/promptex)
4
+ platform adapter, written in TypeScript.
5
+
6
+ **Adapters are language-bound**: this package serves TypeScript prompt projects
7
+ only. To support the same platform from Python or Rust projects, implement the
8
+ same `emit` chain against that language's SDK.
9
+
10
+ The factory returns the same shape as the built-in `claude()` / `codex()`
11
+ targets, so consumers list it right alongside them. Its `emit` function walks a
12
+ fixed responsibility chain:
13
+
14
+ 1. Compute the layout table first — rendering needs it to resolve references.
15
+ 2. Report every unsupported kind via `ctx.unsupported` — never drop silently.
16
+ 3. Render supported entries via `ctx.render(entry, selfPath, layout)`; where
17
+ the platform lacks a feature (e.g. scoped loading), record it with
18
+ `ctx.degrade` and degrade in the output. Read platform-native keys via
19
+ `ctx.overrides`, never from the entry config directly.
20
+ 4. Emit referenced resources.
21
+
22
+ ## Try it
23
+
24
+ ```console
25
+ npm install
26
+ cd example
27
+ npx promptex build --install .
28
+ ```
29
+
30
+ The example project lists `targets: [claude(), target()]`; after install you
31
+ get both `.claude/` and `.{{name}}/` artifacts, and the install report shows
32
+ the `scopedLoading` degradation for the example rule. `npx promptex build --install --dry-run .`
33
+ previews without writing.
34
+
35
+ ## Publishing
36
+
37
+ - `npm run build` compiles `src/` to `dist/`; the package ships `dist/` plus
38
+ `promptex.config.schema.json`.
39
+ - Keep `promptex.config.schema.json` at the package root: `promptex config
40
+ declare promptex-adapter-{{name}}` reads it from there, and `src/index.ts`
41
+ reads that same root copy at load time (a runtime read, not a compile-time
42
+ JSON import, so the emitted tree keeps a single copy and `dist/` stays flat)
43
+ and passes it to `defineTarget`, so it also travels with the intermediate
44
+ representation.
45
+ - Keep the `promptex-adapter-` name prefix so consumers can find the package.
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ # example 具現化驗證時由 promptex 產生(.{{name}}/ 為自訂平台的產物落點)。
4
+ example/.promptex/
5
+ example/.claude/
6
+ example/.{{name}}/
@@ -0,0 +1,12 @@
1
+ // 最小消費專案:自訂 target 與內建 claude() 並列,裝完依賴即可在本目錄執行
2
+ // npx promptex build --install . 驗證兩個平台的產物。真實消費端改為從發布後的套件名
3
+ // import。
4
+ import { claude, defineConfig } from 'promptex-js'
5
+ import target from '../src/index.ts'
6
+
7
+ export default defineConfig({
8
+ name: 'adapter-example',
9
+ srcDir: './prompts',
10
+ outDir: '.',
11
+ targets: [claude(), target()],
12
+ })
@@ -0,0 +1,18 @@
1
+ import { defineRule, defineSkill } from 'promptex-js'
2
+
3
+ export const exampleSkill = defineSkill({
4
+ id: 'adapter-demo-skill',
5
+ name: '{{demoSkillName}}',
6
+ description: '{{adapterDemoSkillDescription}}',
7
+ config: { whenToUse: '{{adapterDemoSkillWhenToUse}}' },
8
+ content: '{{adapterDemoSkillContent}}',
9
+ })
10
+
11
+ // A rule carrying appliesTo: the custom platform has no scoped-loading mechanism,
12
+ // so the install report shows a scopedLoading degrade entry.
13
+ export const exampleRule = defineRule({
14
+ id: 'adapter-demo-rule',
15
+ name: '{{demoRuleName}}',
16
+ config: { appliesTo: ['prompts/**'] },
17
+ content: '{{adapterDemoRuleContent}}',
18
+ })
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "promptex-adapter-{{name}}",
3
+ "version": "0.1.0",
4
+ "description": "A third-party promptex platform adapter.",
5
+ "license": "MIT OR Apache-2.0",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "promptex.config.schema.json"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "tsc"
19
+ },
20
+ "peerDependencies": {
21
+ "promptex-js": "^{{promptexVersion}}"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.10.0",
25
+ "promptex-js": "^{{promptexVersion}}",
26
+ "typescript": "^7.0.2"
27
+ }
28
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "promptex-adapter-{{name}} options",
4
+ "type": "object",
5
+ "properties": {
6
+ "root": {
7
+ "type": "string",
8
+ "description": "Root directory for emitted artifacts, relative to the install root. Defaults to \".{{name}}\"."
9
+ }
10
+ },
11
+ "additionalProperties": false
12
+ }
@@ -0,0 +1,93 @@
1
+ // 第三方 adapter:以 SDK 層實作,只用 AdapterContext 的公開介面。emit 的
2
+ // 責任鏈固定四步:① 算落點表 → ② 回報不支援的 kind → ③ 渲染並產出節點
3
+ // (平台缺某機制時記 degrade 降級)→ ④ 產出被引用的資源。發布前把落點
4
+ // 規則與能力集合換成你的平台實況。
5
+ //
6
+ // adapter 綁語言:本套件服務 TypeScript 專案;Python/Rust 專案要用同一個
7
+ // 平台時,各自以該語言的 SDK 實作一份。
8
+ import { readFileSync } from 'node:fs'
9
+
10
+ import { defineTarget, type AdapterContext, type AdapterTarget, type PluginEntry } from 'promptex-js'
11
+
12
+ // 參數宣告(標準 JSON Schema)住套件根,由本檔在載入時讀進來。用執行期讀檔而非
13
+ // 編譯期 import:JSON 在 include 的 src 之外,靜態 import 會把編譯器的共同來源根
14
+ // 抬到套件根,產物因此多一層 src/,包裡還會留下第二份 schema。改讀檔之後
15
+ // src/index.ts 與 dist/index.js 對套件根的相對位置相同,兩種形態都指向同一份。
16
+ const configSchema = JSON.parse(readFileSync(new URL('../promptex.config.schema.json', import.meta.url), 'utf8'))
17
+
18
+ /** 建構參數:與 promptex.config.schema.json 的宣告一一對應。 */
19
+ export interface Options {
20
+ /** 產物落點的根目錄;預設 `.{{name}}`。 */
21
+ readonly root?: string
22
+ }
23
+
24
+ /** 平台能力:本範例只有提示詞單檔與參考資料,無代理、無事件機制。 */
25
+ const SUPPORTED = ['skill', 'rule', 'instruction'] as const
26
+ const UNSUPPORTED = ['agent', 'hook', 'mcp', 'permission'] as const
27
+
28
+ export default function target(options: Options = {}): AdapterTarget {
29
+ const root = options.root ?? '.{{name}}'
30
+ // 落點規則:提示詞一律單檔平鋪,資源集中於 refs/。
31
+ const nodePath = (e: PluginEntry) => `${root}/prompts/${e.id}.md`
32
+ const resourcePath = (e: PluginEntry) => `${root}/refs/${e.id}.md`
33
+
34
+ const emit = (ctx: AdapterContext): Map<string, string> => {
35
+ const files = new Map<string, string>()
36
+
37
+ // ① 先算落點表:渲染需要它解析引用,故必須在渲染之前完成。
38
+ const layout = new Map<string, string>()
39
+ for (const kind of SUPPORTED) {
40
+ for (const e of ctx.entries(kind)) layout.set(`${kind}:${e.id}`, nodePath(e))
41
+ }
42
+ for (const kind of ['resource', 'asset', 'dir'] as const) {
43
+ for (const e of ctx.entries(kind)) layout.set(`${kind}:${e.id}`, resourcePath(e))
44
+ }
45
+
46
+ // ② 不支援的 kind 逐一列報告,不靜默丟棄。
47
+ for (const kind of UNSUPPORTED) {
48
+ for (const e of ctx.entries(kind)) {
49
+ ctx.unsupported({ kind, nodeId: e.id, feature: kind, note: `{{name}} 無 ${kind} 對應機制,該節點未產出` })
50
+ }
51
+ }
52
+
53
+ // ③ 產出提示詞單檔;rule 的載入範圍在本平台無對應機制,降級為內文標註。
54
+ for (const kind of SUPPORTED) {
55
+ for (const e of ctx.entries(kind)) {
56
+ const path = nodePath(e)
57
+ const cfg = e.def.config as { appliesTo?: readonly string[] } | undefined
58
+ let note = ''
59
+ if (kind === 'rule' && cfg?.appliesTo?.length) {
60
+ note = `適用範圍:${cfg.appliesTo.join('、')}\n\n`
61
+ ctx.degrade({
62
+ kind,
63
+ nodeId: e.id,
64
+ feature: 'scopedLoading',
65
+ note: '{{name}} 無範圍載入機制,改為常駐並於內文標註適用範圍',
66
+ })
67
+ }
68
+ files.set(path, `${frontmatter(ctx, e)}# ${e.name}\n\n${note}${ctx.render(e, path, layout)}\n`)
69
+ }
70
+ }
71
+
72
+ // ④ 被引用的資源。
73
+ for (const e of ctx.entries('resource')) {
74
+ const path = resourcePath(e)
75
+ files.set(path, `# ${e.name}\n\n${ctx.render(e, path, layout)}\n`)
76
+ }
77
+
78
+ return files
79
+ }
80
+
81
+ // 參數宣告隨中介表示交給讀取端;
82
+ // `promptex config declare` 讀的是套件根的同一份檔案。
83
+ return defineTarget('{{name}}', emit, { configSchema })
84
+ }
85
+
86
+ // 平台原生鍵的覆寫:一律經 ctx.overrides 讀取,不自己走
87
+ // entry.def.config.platforms:「空物件視同未宣告」的判定與框架保留鍵
88
+ // (promptex: 前綴)的過濾都由核心同一份實作承載,自己讀會各自重造而漂移。
89
+ const frontmatter = (ctx: AdapterContext, e: PluginEntry): string => {
90
+ const overrides = ctx.overrides(e)
91
+ if (!overrides) return ''
92
+ return `---\n${Object.entries(overrides).map(([k, v]) => `${k}: ${String(v)}`).join('\n')}\n---\n\n`
93
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "nodenext",
4
+ "moduleResolution": "nodenext",
5
+ "target": "es2022",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "skipLibCheck": true,
11
+ "types": ["node"]
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,40 @@
1
+ # promptex-plugin-{{name}}
2
+
3
+ A publishable [promptex](https://github.com/promptex-ai/promptex) plugin,
4
+ written in TypeScript for TypeScript prompt projects.
5
+
6
+ A plugin hooks into the evaluation pipeline in a fixed order:
7
+
8
+ 1. `prepare` — the only async stage, runs before source collection. Fetch
9
+ external data here (cache it under `ctx.cacheDir`, record pins with
10
+ `ctx.writeLock`) and pass it to later stages through the plugin's closure.
11
+ 2. `transform` — rewrite existing entries through the context operations
12
+ (`appendContent` / `patchConfig`) or add new nodes with `define*`.
13
+ 3. `validate` — structural checks over the registry; return diagnostics
14
+ (an empty array passes). Core checks run first.
15
+
16
+ Plugins produce **nodes**, not files: anything you want on disk becomes a
17
+ resource node that the core lays out.
18
+
19
+ ## Try it
20
+
21
+ ```console
22
+ npm install
23
+ cd example
24
+ npx promptex build --install .
25
+ ```
26
+
27
+ The example project wires the plugin in via `plugins: [createPlugin(...)]` and
28
+ shows the banner appended to both entries. `npx promptex build --install --dry-run .` previews
29
+ without writing.
30
+
31
+ ## Publishing
32
+
33
+ - `npm run build` compiles `src/` to `dist/`; the package ships `dist/` plus
34
+ `promptex.config.schema.json`.
35
+ - Keep `promptex.config.schema.json` at the package root: `promptex config
36
+ declare promptex-plugin-{{name}}` reads it from there, and `src/index.ts`
37
+ reads that same root copy at load time (a runtime read, not a compile-time
38
+ JSON import, so the emitted tree keeps a single copy and `dist/` stays flat),
39
+ so the schema also travels with the intermediate representation.
40
+ - Keep the `promptex-plugin-` name prefix so consumers can find the package.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ # example 具現化驗證時由 promptex 產生。
4
+ example/.promptex/
5
+ example/.claude/
@@ -0,0 +1,12 @@
1
+ // 最小消費專案:直接以相對路徑掛上本套件的 plugin,裝完依賴即可在本目錄
2
+ // 執行 npx promptex build --install . 驗證產物。真實消費端改為從發布後的套件名 import。
3
+ import { claude, defineConfig } from 'promptex-js'
4
+ import { createPlugin } from '../src/index.ts'
5
+
6
+ export default defineConfig({
7
+ name: 'plugin-example',
8
+ srcDir: './prompts',
9
+ outDir: '.',
10
+ targets: [claude()],
11
+ plugins: [createPlugin({ banner: '(由 {{name}} plugin 追加)' })],
12
+ })
@@ -0,0 +1,16 @@
1
+ import { defineRule, defineSkill } from 'promptex-js'
2
+
3
+ export const exampleSkill = defineSkill({
4
+ id: 'plugin-demo-skill',
5
+ name: '{{demoSkillName}}',
6
+ description: '{{pluginDemoSkillDescription}}',
7
+ config: { whenToUse: '{{pluginDemoSkillWhenToUse}}' },
8
+ content: '{{pluginDemoSkillContent}}',
9
+ })
10
+
11
+ export const exampleRule = defineRule({
12
+ id: 'plugin-demo-rule',
13
+ name: '{{demoRuleName}}',
14
+ config: { appliesTo: ['prompts/**'] },
15
+ content: '{{pluginDemoRuleContent}}',
16
+ })