@portel/photon 1.20.1 → 1.22.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.
- package/README.md +5 -5
- package/dist/ag-ui/adapter.d.ts +4 -1
- package/dist/ag-ui/adapter.d.ts.map +1 -1
- package/dist/ag-ui/adapter.js +58 -3
- package/dist/ag-ui/adapter.js.map +1 -1
- package/dist/ag-ui/types.d.ts +12 -0
- package/dist/ag-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +8 -49
- package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.d.ts +1 -1
- package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.js +79 -1
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +23 -31
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts.map +1 -1
- package/dist/auto-ui/bridge/index.js +107 -11
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/bridge/renderers.d.ts +14 -0
- package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
- package/dist/auto-ui/bridge/renderers.js +680 -57
- package/dist/auto-ui/bridge/renderers.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +3 -3
- package/dist/auto-ui/frontend/pure-view.html +19 -19
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +53 -2
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/ui-resolver.d.ts +25 -0
- package/dist/auto-ui/ui-resolver.d.ts.map +1 -0
- package/dist/auto-ui/ui-resolver.js +95 -0
- package/dist/auto-ui/ui-resolver.js.map +1 -0
- package/dist/beam-form.bundle.js +7 -7
- package/dist/beam-form.bundle.js.map +1 -1
- package/dist/beam.bundle.js +905 -185
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +9 -5
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +93 -53
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/publish.d.ts +14 -0
- package/dist/cli/commands/publish.d.ts.map +1 -0
- package/dist/cli/commands/publish.js +126 -0
- package/dist/cli/commands/publish.js.map +1 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +2 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +17 -5
- package/dist/context.js.map +1 -1
- package/dist/daemon/client.d.ts +9 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +54 -1
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +3 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +88 -38
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/ownership.d.ts +12 -0
- package/dist/daemon/ownership.d.ts.map +1 -0
- package/dist/daemon/ownership.js +55 -0
- package/dist/daemon/ownership.js.map +1 -0
- package/dist/daemon/protocol.d.ts +4 -2
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +15 -2
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.js +557 -83
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +9 -1
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +54 -1
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/daemon/worker-manager.d.ts +12 -0
- package/dist/daemon/worker-manager.d.ts.map +1 -1
- package/dist/daemon/worker-manager.js +89 -6
- package/dist/daemon/worker-manager.js.map +1 -1
- package/dist/loader.d.ts +17 -9
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +415 -141
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +26 -2
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photons/canvas/ui/canvas.photon.html +1493 -0
- package/dist/photons/canvas.photon.d.ts +400 -0
- package/dist/photons/canvas.photon.d.ts.map +1 -0
- package/dist/photons/canvas.photon.js +662 -0
- package/dist/photons/canvas.photon.js.map +1 -0
- package/dist/photons/canvas.photon.ts +814 -0
- package/dist/photons/publish.photon.d.ts +97 -0
- package/dist/photons/publish.photon.d.ts.map +1 -0
- package/dist/photons/publish.photon.js +569 -0
- package/dist/photons/publish.photon.js.map +1 -0
- package/dist/photons/publish.photon.ts +683 -0
- package/dist/photons/ui/canvas.photon.html +624 -0
- package/dist/resource-server.d.ts.map +1 -1
- package/dist/resource-server.js +7 -1
- package/dist/resource-server.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +67 -37
- package/dist/server.js.map +1 -1
- package/dist/shared/error-handler.d.ts +1 -0
- package/dist/shared/error-handler.d.ts.map +1 -1
- package/dist/shared/error-handler.js +68 -10
- package/dist/shared/error-handler.js.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/logger.js +34 -0
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared-utils.d.ts.map +1 -1
- package/dist/shared-utils.js +2 -2
- package/dist/shared-utils.js.map +1 -1
- package/dist/telemetry/context.d.ts +24 -0
- package/dist/telemetry/context.d.ts.map +1 -0
- package/dist/telemetry/context.js +17 -0
- package/dist/telemetry/context.js.map +1 -0
- package/dist/telemetry/logs.d.ts +38 -0
- package/dist/telemetry/logs.d.ts.map +1 -0
- package/dist/telemetry/logs.js +108 -0
- package/dist/telemetry/logs.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +71 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +184 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/otel.d.ts +20 -1
- package/dist/telemetry/otel.d.ts.map +1 -1
- package/dist/telemetry/otel.js +79 -2
- package/dist/telemetry/otel.js.map +1 -1
- package/dist/telemetry/sdk.d.ts +49 -0
- package/dist/telemetry/sdk.d.ts.map +1 -0
- package/dist/telemetry/sdk.js +110 -0
- package/dist/telemetry/sdk.js.map +1 -0
- package/dist/tsx-compiler.d.ts +23 -0
- package/dist/tsx-compiler.d.ts.map +1 -0
- package/dist/tsx-compiler.js +221 -0
- package/dist/tsx-compiler.js.map +1 -0
- package/package.json +7 -7
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photon Publish - Publish your photons as a marketplace
|
|
3
|
+
* @description System photon: init → sync → bump versions → commit → push → share
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* Dog-foods the wizard/elicitation pattern. Each step yields either an `ask:*`
|
|
7
|
+
* (elicits user input) or an `emit:*` (reports progress/result). Drivers — CLI,
|
|
8
|
+
* Beam, MCP — handle yields in their own way; the photon stays surface-agnostic.
|
|
9
|
+
*
|
|
10
|
+
* Optional dependency on code-diagram: if installed, publish can use it for
|
|
11
|
+
* richer diff/suggestion logic in a future version. v1 uses a regex-based
|
|
12
|
+
* heuristic so the photon is useful on its own.
|
|
13
|
+
*
|
|
14
|
+
* @photon codeDiagram code-diagram?
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'fs/promises';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { execFile } from 'child_process';
|
|
20
|
+
import { existsSync } from 'fs';
|
|
21
|
+
import { promisify } from 'util';
|
|
22
|
+
const execFileAsync = promisify(execFile);
|
|
23
|
+
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
// Types
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
|
|
28
|
+
type BumpLevel = 'patch' | 'minor' | 'major';
|
|
29
|
+
|
|
30
|
+
type WizardStep =
|
|
31
|
+
| {
|
|
32
|
+
ask: 'text';
|
|
33
|
+
id: string;
|
|
34
|
+
message: string;
|
|
35
|
+
label?: string;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
hint?: string;
|
|
38
|
+
required?: boolean;
|
|
39
|
+
default?: string;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
ask: 'select';
|
|
43
|
+
id: string;
|
|
44
|
+
message: string;
|
|
45
|
+
options: Array<{ value: string; label: string }>;
|
|
46
|
+
default?: string;
|
|
47
|
+
}
|
|
48
|
+
| { ask: 'confirm'; id: string; message: string; default?: boolean }
|
|
49
|
+
| { emit: 'status'; message: string; data?: unknown }
|
|
50
|
+
| { emit: 'result'; data: unknown };
|
|
51
|
+
|
|
52
|
+
interface PhotonChange {
|
|
53
|
+
file: string;
|
|
54
|
+
name: string;
|
|
55
|
+
status: 'new' | 'changed' | 'unchanged';
|
|
56
|
+
currentVersion: string;
|
|
57
|
+
suggestedBump: BumpLevel;
|
|
58
|
+
suggestedVersion: string;
|
|
59
|
+
chosenBump?: BumpLevel | 'skip';
|
|
60
|
+
addedMethods: string[];
|
|
61
|
+
removedMethods: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PublishParams {
|
|
65
|
+
dir?: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
owner?: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
bump?: BumpLevel | 'auto';
|
|
70
|
+
public?: boolean;
|
|
71
|
+
dryRun?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
// Pure helpers (exported for tests)
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
export function parseVersionTag(source: string): string {
|
|
79
|
+
const m = source.match(/@version\s+(\S+)/);
|
|
80
|
+
return m ? m[1] : '0.0.0';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function bumpVersion(current: string, level: BumpLevel): string {
|
|
84
|
+
const parts = current.split('.').map((n) => parseInt(n, 10));
|
|
85
|
+
while (parts.length < 3) parts.push(0);
|
|
86
|
+
const [maj, min, pat] = parts.map((n) => (isNaN(n) ? 0 : n));
|
|
87
|
+
if (level === 'major') return `${maj + 1}.0.0`;
|
|
88
|
+
if (level === 'minor') return `${maj}.${min + 1}.0`;
|
|
89
|
+
return `${maj}.${min}.${pat + 1}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function writeVersionTag(source: string, nextVersion: string): string {
|
|
93
|
+
if (/@version\s+\S+/.test(source)) {
|
|
94
|
+
return source.replace(/@version\s+\S+/, `@version ${nextVersion}`);
|
|
95
|
+
}
|
|
96
|
+
if (/^\/\*\*/.test(source)) {
|
|
97
|
+
return source.replace(/\*\//, ` * @version ${nextVersion}\n */`);
|
|
98
|
+
}
|
|
99
|
+
return `/** @version ${nextVersion} */\n${source}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function extractMethodNames(source: string): Set<string> {
|
|
103
|
+
const methods = new Set<string>();
|
|
104
|
+
// Match method declarations: preceded by `{`, `;`, or whitespace; optional
|
|
105
|
+
// modifiers; captured name; args list; then `{` (function body) — the
|
|
106
|
+
// trailing `{` is what distinguishes declarations from call sites.
|
|
107
|
+
const re =
|
|
108
|
+
/(?<=[{\s;])(?:public\s+|protected\s+|private\s+|static\s+|async\s+)*\*?\s*(\w+)\s*\([^)]*\)\s*(?::\s*[^{;]+)?\s*\{/g;
|
|
109
|
+
const keywords = new Set([
|
|
110
|
+
'constructor',
|
|
111
|
+
'if',
|
|
112
|
+
'for',
|
|
113
|
+
'while',
|
|
114
|
+
'switch',
|
|
115
|
+
'return',
|
|
116
|
+
'catch',
|
|
117
|
+
'do',
|
|
118
|
+
'else',
|
|
119
|
+
'function',
|
|
120
|
+
]);
|
|
121
|
+
let m: RegExpExecArray | null;
|
|
122
|
+
while ((m = re.exec(source)) !== null) {
|
|
123
|
+
const name = m[1];
|
|
124
|
+
if (keywords.has(name)) continue;
|
|
125
|
+
if (name.startsWith('_')) continue;
|
|
126
|
+
methods.add(name);
|
|
127
|
+
}
|
|
128
|
+
return methods;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function suggestBumpFromDiff(
|
|
132
|
+
beforeSource: string | null,
|
|
133
|
+
afterSource: string
|
|
134
|
+
): { level: BumpLevel; added: string[]; removed: string[] } {
|
|
135
|
+
if (beforeSource === null) {
|
|
136
|
+
return { level: 'minor', added: [...extractMethodNames(afterSource)], removed: [] };
|
|
137
|
+
}
|
|
138
|
+
const before = extractMethodNames(beforeSource);
|
|
139
|
+
const after = extractMethodNames(afterSource);
|
|
140
|
+
const added = [...after].filter((m) => !before.has(m));
|
|
141
|
+
const removed = [...before].filter((m) => !after.has(m));
|
|
142
|
+
let level: BumpLevel = 'patch';
|
|
143
|
+
if (removed.length > 0) level = 'major';
|
|
144
|
+
else if (added.length > 0) level = 'minor';
|
|
145
|
+
return { level, added, removed };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function parseOwnerRepo(remote: string): string | null {
|
|
149
|
+
const m = remote.match(/github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
150
|
+
return m ? m[1] : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
154
|
+
// Process helpers (inlined — photon cannot import runtime shared modules)
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
|
|
157
|
+
async function run(cmd: string, args: string[], cwd: string): Promise<string> {
|
|
158
|
+
const { stdout } = await execFileAsync(cmd, args, { cwd });
|
|
159
|
+
return stdout.trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function tryRun(cmd: string, args: string[], cwd: string): Promise<string | null> {
|
|
163
|
+
try {
|
|
164
|
+
return await run(cmd, args, cwd);
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function commandExists(bin: string): Promise<boolean> {
|
|
171
|
+
try {
|
|
172
|
+
await execFileAsync('which', [bin]);
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
180
|
+
// Change collection (exported for tests)
|
|
181
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
182
|
+
|
|
183
|
+
export async function collectChanges(cwd: string): Promise<PhotonChange[]> {
|
|
184
|
+
const all = (await fs.readdir(cwd)).filter((f) => f.endsWith('.photon.ts'));
|
|
185
|
+
|
|
186
|
+
const changedFiles = new Set<string>();
|
|
187
|
+
const untrackedSet = new Set<string>();
|
|
188
|
+
|
|
189
|
+
const tracked = await tryRun('git', ['diff', '--name-only', 'HEAD', '--', '*.photon.ts'], cwd);
|
|
190
|
+
if (tracked) {
|
|
191
|
+
tracked
|
|
192
|
+
.split('\n')
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
.forEach((f) => changedFiles.add(path.basename(f)));
|
|
195
|
+
}
|
|
196
|
+
const untracked = await tryRun(
|
|
197
|
+
'git',
|
|
198
|
+
['ls-files', '--others', '--exclude-standard', '--', '*.photon.ts'],
|
|
199
|
+
cwd
|
|
200
|
+
);
|
|
201
|
+
if (untracked) {
|
|
202
|
+
untracked
|
|
203
|
+
.split('\n')
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.forEach((f) => {
|
|
206
|
+
const b = path.basename(f);
|
|
207
|
+
changedFiles.add(b);
|
|
208
|
+
untrackedSet.add(b);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const changes: PhotonChange[] = [];
|
|
213
|
+
for (const file of all) {
|
|
214
|
+
const afterSrc = await fs.readFile(path.join(cwd, file), 'utf-8');
|
|
215
|
+
const currentVersion = parseVersionTag(afterSrc);
|
|
216
|
+
const isNew = untrackedSet.has(file);
|
|
217
|
+
const isChanged = changedFiles.has(file);
|
|
218
|
+
|
|
219
|
+
if (!isNew && !isChanged) {
|
|
220
|
+
changes.push({
|
|
221
|
+
file,
|
|
222
|
+
name: file.replace(/\.photon\.ts$/, ''),
|
|
223
|
+
status: 'unchanged',
|
|
224
|
+
currentVersion,
|
|
225
|
+
suggestedBump: 'patch',
|
|
226
|
+
suggestedVersion: currentVersion,
|
|
227
|
+
addedMethods: [],
|
|
228
|
+
removedMethods: [],
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const beforeSrc = isNew ? null : await tryRun('git', ['show', `HEAD:${file}`], cwd);
|
|
234
|
+
const { level, added, removed } = suggestBumpFromDiff(beforeSrc, afterSrc);
|
|
235
|
+
|
|
236
|
+
changes.push({
|
|
237
|
+
file,
|
|
238
|
+
name: file.replace(/\.photon\.ts$/, ''),
|
|
239
|
+
status: isNew ? 'new' : 'changed',
|
|
240
|
+
currentVersion,
|
|
241
|
+
suggestedBump: level,
|
|
242
|
+
suggestedVersion: isNew
|
|
243
|
+
? currentVersion === '0.0.0'
|
|
244
|
+
? '0.1.0'
|
|
245
|
+
: currentVersion
|
|
246
|
+
: bumpVersion(currentVersion, level),
|
|
247
|
+
addedMethods: added,
|
|
248
|
+
removedMethods: removed,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return changes;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
255
|
+
// The Publish photon
|
|
256
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
257
|
+
|
|
258
|
+
export default class Publish {
|
|
259
|
+
/**
|
|
260
|
+
* Publish your photons as a marketplace — interactive wizard
|
|
261
|
+
*
|
|
262
|
+
* @wizard
|
|
263
|
+
* @param dir Directory to publish (defaults to current working directory)
|
|
264
|
+
* @param name Marketplace name (skips elicitation)
|
|
265
|
+
* @param owner GitHub owner (skips elicitation)
|
|
266
|
+
* @param description Marketplace description
|
|
267
|
+
* @param bump Version bump: patch | minor | major | auto (skips per-photon elicitation)
|
|
268
|
+
* @param public Create remote as public (default: private)
|
|
269
|
+
* @param dryRun Show plan without touching anything
|
|
270
|
+
*/
|
|
271
|
+
static async *wizard(params: PublishParams = {}): AsyncGenerator<WizardStep, void, any> {
|
|
272
|
+
const cwd = path.resolve(params.dir || process.cwd());
|
|
273
|
+
const dry = params.dryRun === true;
|
|
274
|
+
|
|
275
|
+
yield { emit: 'status', message: `📦 Publishing from ${cwd}${dry ? ' (dry run)' : ''}` };
|
|
276
|
+
|
|
277
|
+
// ── Phase 0: preflight ────────────────────────────────────────────────
|
|
278
|
+
if (!existsSync(path.join(cwd, '.git'))) {
|
|
279
|
+
const initAnswer = yield {
|
|
280
|
+
ask: 'confirm',
|
|
281
|
+
id: 'git-init',
|
|
282
|
+
message: 'Not a git repo here. Initialize one?',
|
|
283
|
+
default: true,
|
|
284
|
+
};
|
|
285
|
+
if (initAnswer !== true) {
|
|
286
|
+
yield { emit: 'result', data: { cancelled: true, reason: 'no-git' } };
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (!dry) await run('git', ['init'], cwd);
|
|
290
|
+
yield { emit: 'status', message: '✅ git initialized' };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const photonFiles = (await fs.readdir(cwd)).filter((f) => f.endsWith('.photon.ts'));
|
|
294
|
+
if (photonFiles.length === 0) {
|
|
295
|
+
yield {
|
|
296
|
+
emit: 'result',
|
|
297
|
+
data: {
|
|
298
|
+
error: 'No .photon.ts files in this directory.',
|
|
299
|
+
hint: 'Create one first: photon cli maker new <name>',
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!(await commandExists('git'))) {
|
|
306
|
+
yield { emit: 'result', data: { error: 'git not found. Install git first.' } };
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const hasGh = await commandExists('gh');
|
|
310
|
+
|
|
311
|
+
// ── Phase 1: marketplace init ─────────────────────────────────────────
|
|
312
|
+
const marketplaceDir = path.join(cwd, '.marketplace');
|
|
313
|
+
let marketplaceName = params.name;
|
|
314
|
+
let owner = params.owner;
|
|
315
|
+
const description = params.description;
|
|
316
|
+
|
|
317
|
+
if (!existsSync(marketplaceDir)) {
|
|
318
|
+
yield { emit: 'status', message: "No marketplace manifest — let's set one up." };
|
|
319
|
+
|
|
320
|
+
if (!marketplaceName) {
|
|
321
|
+
const nameVal = yield {
|
|
322
|
+
ask: 'text',
|
|
323
|
+
id: 'marketplace-name',
|
|
324
|
+
message: 'Marketplace name',
|
|
325
|
+
default: path.basename(cwd),
|
|
326
|
+
placeholder: path.basename(cwd),
|
|
327
|
+
required: true,
|
|
328
|
+
};
|
|
329
|
+
marketplaceName = (typeof nameVal === 'string' && nameVal.trim()) || path.basename(cwd);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!owner) {
|
|
333
|
+
const detected =
|
|
334
|
+
(await tryRun('gh', ['api', 'user', '-q', '.login'], cwd)) ||
|
|
335
|
+
(await tryRun('git', ['config', 'user.name'], cwd)) ||
|
|
336
|
+
'';
|
|
337
|
+
const ownerVal = yield {
|
|
338
|
+
ask: 'text',
|
|
339
|
+
id: 'owner',
|
|
340
|
+
message: 'GitHub owner',
|
|
341
|
+
default: detected,
|
|
342
|
+
placeholder: detected,
|
|
343
|
+
required: true,
|
|
344
|
+
};
|
|
345
|
+
owner = (typeof ownerVal === 'string' && ownerVal.trim()) || detected;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!owner) {
|
|
349
|
+
yield { emit: 'result', data: { error: 'Owner (GitHub username) is required.' } };
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
yield { emit: 'status', message: `Initializing marketplace: ${marketplaceName} (${owner})` };
|
|
354
|
+
if (!dry) {
|
|
355
|
+
await Publish.performMarketplaceInit(cwd, {
|
|
356
|
+
name: marketplaceName,
|
|
357
|
+
owner,
|
|
358
|
+
description: description || '',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Phase 2: scan & diff ──────────────────────────────────────────────
|
|
364
|
+
yield { emit: 'status', message: 'Scanning photons for changes...' };
|
|
365
|
+
const changes = await collectChanges(cwd);
|
|
366
|
+
const publishable = changes.filter((c) => c.status !== 'unchanged');
|
|
367
|
+
|
|
368
|
+
if (publishable.length === 0) {
|
|
369
|
+
yield {
|
|
370
|
+
emit: 'result',
|
|
371
|
+
data: { published: 0, message: 'Nothing to publish — all photons unchanged.' },
|
|
372
|
+
};
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
yield {
|
|
377
|
+
emit: 'status',
|
|
378
|
+
message: `Found ${publishable.length} photon(s) to publish`,
|
|
379
|
+
data: {
|
|
380
|
+
changes: publishable.map((c) => ({
|
|
381
|
+
name: c.name,
|
|
382
|
+
status: c.status,
|
|
383
|
+
current: c.currentVersion,
|
|
384
|
+
suggested: c.suggestedVersion,
|
|
385
|
+
added: c.addedMethods,
|
|
386
|
+
removed: c.removedMethods,
|
|
387
|
+
})),
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const confirmPublish = yield {
|
|
392
|
+
ask: 'confirm',
|
|
393
|
+
id: 'confirm-publish',
|
|
394
|
+
message: `Publish ${publishable.length} photon(s)?`,
|
|
395
|
+
default: true,
|
|
396
|
+
};
|
|
397
|
+
if (confirmPublish !== true) {
|
|
398
|
+
yield { emit: 'result', data: { cancelled: true, atStep: 'confirm-publish' } };
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Phase 2.5: version bump elicitation ───────────────────────────────
|
|
403
|
+
for (const c of publishable) {
|
|
404
|
+
let chosen: BumpLevel | 'skip' = c.suggestedBump;
|
|
405
|
+
|
|
406
|
+
if (params.bump === 'auto') {
|
|
407
|
+
chosen = c.suggestedBump;
|
|
408
|
+
} else if (params.bump) {
|
|
409
|
+
chosen = params.bump;
|
|
410
|
+
} else {
|
|
411
|
+
const reason = c.removedMethods.length
|
|
412
|
+
? `removed: ${c.removedMethods.join(', ')}`
|
|
413
|
+
: c.addedMethods.length
|
|
414
|
+
? `new methods: ${c.addedMethods.join(', ')}`
|
|
415
|
+
: 'internal changes only';
|
|
416
|
+
|
|
417
|
+
const selection = yield {
|
|
418
|
+
ask: 'select',
|
|
419
|
+
id: `bump-${c.name}`,
|
|
420
|
+
message: `${c.name} ${c.currentVersion} — ${reason}. Bump how?`,
|
|
421
|
+
default: c.suggestedBump,
|
|
422
|
+
options: [
|
|
423
|
+
{
|
|
424
|
+
value: c.suggestedBump,
|
|
425
|
+
label: `${c.suggestedBump} → ${c.suggestedVersion} (recommended)`,
|
|
426
|
+
},
|
|
427
|
+
{ value: 'patch', label: `patch → ${bumpVersion(c.currentVersion, 'patch')}` },
|
|
428
|
+
{ value: 'minor', label: `minor → ${bumpVersion(c.currentVersion, 'minor')}` },
|
|
429
|
+
{ value: 'major', label: `major → ${bumpVersion(c.currentVersion, 'major')}` },
|
|
430
|
+
{ value: 'skip', label: `skip (keep ${c.currentVersion})` },
|
|
431
|
+
],
|
|
432
|
+
};
|
|
433
|
+
chosen = (selection as BumpLevel | 'skip') || c.suggestedBump;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
c.chosenBump = chosen;
|
|
437
|
+
if (chosen !== 'skip' && c.status !== 'new') {
|
|
438
|
+
c.suggestedVersion = bumpVersion(c.currentVersion, chosen);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (chosen !== 'skip' && !dry) {
|
|
442
|
+
const filePath = path.join(cwd, c.file);
|
|
443
|
+
const src = await fs.readFile(filePath, 'utf-8');
|
|
444
|
+
const next = writeVersionTag(src, c.suggestedVersion);
|
|
445
|
+
if (next !== src) await fs.writeFile(filePath, next, 'utf-8');
|
|
446
|
+
yield {
|
|
447
|
+
emit: 'status',
|
|
448
|
+
message: `✓ ${c.name}: ${c.currentVersion} → ${c.suggestedVersion}`,
|
|
449
|
+
};
|
|
450
|
+
} else if (chosen === 'skip') {
|
|
451
|
+
yield { emit: 'status', message: `– ${c.name}: kept ${c.currentVersion}` };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ── Phase 3: commit & push ────────────────────────────────────────────
|
|
456
|
+
if (!dry) {
|
|
457
|
+
yield { emit: 'status', message: 'Syncing manifest...' };
|
|
458
|
+
await Publish.performMarketplaceSync(cwd, { name: marketplaceName, owner, description });
|
|
459
|
+
|
|
460
|
+
for (const f of photonFiles) await tryRun('git', ['add', '--', f], cwd);
|
|
461
|
+
await tryRun('git', ['add', '--', '.marketplace'], cwd);
|
|
462
|
+
await tryRun('git', ['add', '--', 'README.md'], cwd);
|
|
463
|
+
await tryRun('git', ['add', '--', '.claude-plugin'], cwd);
|
|
464
|
+
await tryRun('git', ['add', '--', '.githooks'], cwd);
|
|
465
|
+
await tryRun('git', ['add', '--', '.gitignore'], cwd);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const staged = (await tryRun('git', ['diff', '--cached', '--name-only'], cwd)) || '';
|
|
469
|
+
if (!staged.trim() && !dry) {
|
|
470
|
+
yield { emit: 'result', data: { published: 0, message: 'Nothing staged after sync.' } };
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const defaultMsg = `chore: publish ${publishable.length} photon${publishable.length === 1 ? '' : 's'}`;
|
|
475
|
+
const msgVal = yield {
|
|
476
|
+
ask: 'text',
|
|
477
|
+
id: 'commit-message',
|
|
478
|
+
message: 'Commit message',
|
|
479
|
+
default: defaultMsg,
|
|
480
|
+
placeholder: defaultMsg,
|
|
481
|
+
};
|
|
482
|
+
const commitMsg = (typeof msgVal === 'string' && msgVal.trim()) || defaultMsg;
|
|
483
|
+
|
|
484
|
+
const confirmCommit = yield {
|
|
485
|
+
ask: 'confirm',
|
|
486
|
+
id: 'confirm-commit',
|
|
487
|
+
message: `Commit as "${commitMsg}"?`,
|
|
488
|
+
default: true,
|
|
489
|
+
};
|
|
490
|
+
if (confirmCommit !== true) {
|
|
491
|
+
yield { emit: 'result', data: { cancelled: true, atStep: 'confirm-commit' } };
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!dry) {
|
|
496
|
+
await run('git', ['commit', '-m', commitMsg], cwd);
|
|
497
|
+
yield { emit: 'status', message: '✅ Committed' };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Remote handling
|
|
501
|
+
const branch = (await tryRun('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd)) || 'main';
|
|
502
|
+
const hasOrigin = !!(await tryRun('git', ['remote', 'get-url', 'origin'], cwd));
|
|
503
|
+
|
|
504
|
+
if (!hasOrigin) {
|
|
505
|
+
const choice = yield {
|
|
506
|
+
ask: 'select',
|
|
507
|
+
id: 'remote-choice',
|
|
508
|
+
message: 'No `origin` remote. What next?',
|
|
509
|
+
default: 'gh',
|
|
510
|
+
options: [
|
|
511
|
+
{ value: 'gh', label: 'Create GitHub repo via gh' },
|
|
512
|
+
{ value: 'manual', label: "I'll add the remote manually" },
|
|
513
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
514
|
+
],
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
if (choice === 'cancel' || choice === 'manual') {
|
|
518
|
+
yield {
|
|
519
|
+
emit: 'result',
|
|
520
|
+
data: {
|
|
521
|
+
cancelled: choice === 'cancel',
|
|
522
|
+
hint:
|
|
523
|
+
choice === 'manual'
|
|
524
|
+
? `Add remote, then push: git push -u origin ${branch}`
|
|
525
|
+
: undefined,
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!hasGh) {
|
|
532
|
+
yield {
|
|
533
|
+
emit: 'result',
|
|
534
|
+
data: { error: 'gh not installed — install from https://cli.github.com/' },
|
|
535
|
+
};
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const suggested =
|
|
540
|
+
owner && marketplaceName ? `${owner}/${marketplaceName}` : path.basename(cwd);
|
|
541
|
+
const repoVal = yield {
|
|
542
|
+
ask: 'text',
|
|
543
|
+
id: 'repo-name',
|
|
544
|
+
message: 'Repository (owner/name)',
|
|
545
|
+
default: suggested,
|
|
546
|
+
placeholder: suggested,
|
|
547
|
+
required: true,
|
|
548
|
+
};
|
|
549
|
+
const repo = (typeof repoVal === 'string' && repoVal.trim()) || suggested;
|
|
550
|
+
|
|
551
|
+
const makePublic =
|
|
552
|
+
params.public === true ||
|
|
553
|
+
(yield {
|
|
554
|
+
ask: 'confirm',
|
|
555
|
+
id: 'make-public',
|
|
556
|
+
message: 'Make this repo public so others can install it?',
|
|
557
|
+
default: false,
|
|
558
|
+
}) === true;
|
|
559
|
+
|
|
560
|
+
if (!dry) {
|
|
561
|
+
const vis = makePublic ? '--public' : '--private';
|
|
562
|
+
await run(
|
|
563
|
+
'gh',
|
|
564
|
+
['repo', 'create', repo, vis, '--source=.', '--remote=origin', '--push'],
|
|
565
|
+
cwd
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
yield {
|
|
569
|
+
emit: 'status',
|
|
570
|
+
message: `✅ Created ${makePublic ? 'public' : 'private'} repo ${repo} and pushed`,
|
|
571
|
+
};
|
|
572
|
+
} else if (!dry) {
|
|
573
|
+
try {
|
|
574
|
+
await run('git', ['push', '-u', 'origin', branch], cwd);
|
|
575
|
+
yield { emit: 'status', message: `✅ Pushed to origin/${branch}` };
|
|
576
|
+
} catch (err: any) {
|
|
577
|
+
yield {
|
|
578
|
+
emit: 'result',
|
|
579
|
+
data: {
|
|
580
|
+
error: `Push failed: ${err?.message || String(err)}`,
|
|
581
|
+
hint: 'Resolve (e.g. `git pull --rebase`), then re-run publish.',
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ── Phase 4: share ────────────────────────────────────────────────────
|
|
589
|
+
const originUrl = (await tryRun('git', ['remote', 'get-url', 'origin'], cwd)) || '';
|
|
590
|
+
const ownerRepo = parseOwnerRepo(originUrl);
|
|
591
|
+
|
|
592
|
+
yield {
|
|
593
|
+
emit: 'result',
|
|
594
|
+
data: {
|
|
595
|
+
published: publishable.length,
|
|
596
|
+
bumps: publishable.map((c) => ({
|
|
597
|
+
name: c.name,
|
|
598
|
+
from: c.currentVersion,
|
|
599
|
+
to: c.chosenBump === 'skip' ? c.currentVersion : c.suggestedVersion,
|
|
600
|
+
chosen: c.chosenBump,
|
|
601
|
+
})),
|
|
602
|
+
repoUrl: ownerRepo ? `https://github.com/${ownerRepo}` : null,
|
|
603
|
+
installCommand: ownerRepo
|
|
604
|
+
? `photon marketplace add ${ownerRepo} && photon add <photon>`
|
|
605
|
+
: null,
|
|
606
|
+
next: [
|
|
607
|
+
'Iterate and re-run `photon publish`',
|
|
608
|
+
'Contribute to others: `photon contribute <name>`',
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
615
|
+
// Internals — inlined marketplace helpers (cannot import from src/cli/…)
|
|
616
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
617
|
+
|
|
618
|
+
private static async performMarketplaceInit(
|
|
619
|
+
cwd: string,
|
|
620
|
+
opts: { name: string; owner: string; description: string }
|
|
621
|
+
): Promise<void> {
|
|
622
|
+
await fs.mkdir(path.join(cwd, '.marketplace'), { recursive: true });
|
|
623
|
+
const manifestPath = path.join(cwd, '.marketplace', 'photons.json');
|
|
624
|
+
const manifest = {
|
|
625
|
+
name: opts.name,
|
|
626
|
+
owner: opts.owner,
|
|
627
|
+
description: opts.description,
|
|
628
|
+
photons: [],
|
|
629
|
+
};
|
|
630
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
631
|
+
|
|
632
|
+
// Ensure .data/ gitignored
|
|
633
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
634
|
+
let existing = '';
|
|
635
|
+
try {
|
|
636
|
+
existing = await fs.readFile(gitignorePath, 'utf-8');
|
|
637
|
+
} catch {
|
|
638
|
+
/* none */
|
|
639
|
+
}
|
|
640
|
+
const patterns = ['.data/', 'node_modules/', '.DS_Store'];
|
|
641
|
+
const lines = existing.split('\n').map((l) => l.trim());
|
|
642
|
+
const missing = patterns.filter((p) => !lines.includes(p));
|
|
643
|
+
if (missing.length > 0) {
|
|
644
|
+
const append = (existing && !existing.endsWith('\n') ? '\n' : '') + missing.join('\n') + '\n';
|
|
645
|
+
await fs.appendFile(gitignorePath, append);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private static async performMarketplaceSync(
|
|
650
|
+
cwd: string,
|
|
651
|
+
opts: { name?: string; owner?: string; description?: string }
|
|
652
|
+
): Promise<void> {
|
|
653
|
+
const manifestPath = path.join(cwd, '.marketplace', 'photons.json');
|
|
654
|
+
let manifest: any = {
|
|
655
|
+
name: opts.name || path.basename(cwd),
|
|
656
|
+
owner: opts.owner || '',
|
|
657
|
+
description: opts.description || '',
|
|
658
|
+
photons: [],
|
|
659
|
+
};
|
|
660
|
+
try {
|
|
661
|
+
manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
662
|
+
} catch {
|
|
663
|
+
/* fresh */
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const files = (await fs.readdir(cwd)).filter((f) => f.endsWith('.photon.ts'));
|
|
667
|
+
manifest.photons = [];
|
|
668
|
+
for (const f of files) {
|
|
669
|
+
const src = await fs.readFile(path.join(cwd, f), 'utf-8');
|
|
670
|
+
const version = parseVersionTag(src);
|
|
671
|
+
const descMatch = src.match(/@description\s+(.+)/);
|
|
672
|
+
manifest.photons.push({
|
|
673
|
+
name: f.replace(/\.photon\.ts$/, ''),
|
|
674
|
+
file: f,
|
|
675
|
+
version,
|
|
676
|
+
description: descMatch ? descMatch[1].trim() : '',
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
681
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
682
|
+
}
|
|
683
|
+
}
|