phewsh 0.3.0 โ 0.5.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/bin/phewsh.js +21 -12
- package/commands/clarify.js +233 -0
- package/commands/intent.js +24 -80
- package/commands/link.js +4 -0
- package/commands/pull.js +4 -0
- package/commands/push.js +4 -0
- package/commands/sync.js +106 -24
- package/lib/pps.js +158 -0
- package/package.json +1 -1
package/bin/phewsh.js
CHANGED
|
@@ -28,7 +28,8 @@ function showBrand() {
|
|
|
28
28
|
console.log('');
|
|
29
29
|
console.log(` ${d('๐ฎ\u200d๐จ')} ${d('๐คซ')}`);
|
|
30
30
|
console.log('');
|
|
31
|
-
console.log(` ${b(w('
|
|
31
|
+
console.log(` ${b(w('โโโ โโโ โโโ โโโ โโ โโโ'))}`);
|
|
32
|
+
console.log(` ${b(w('โโโ โโโ โโโ โโโ โโ โโโ'))}`);
|
|
32
33
|
console.log(` ${g('Build with clarity. Execute without drift.')}`);
|
|
33
34
|
console.log('');
|
|
34
35
|
console.log(hint);
|
|
@@ -36,11 +37,15 @@ function showBrand() {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const COMMANDS = {
|
|
39
|
-
intent:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
intent: () => require('../commands/intent'),
|
|
41
|
+
clarify: () => require('../commands/clarify'),
|
|
42
|
+
push: () => require('../commands/push'),
|
|
43
|
+
pull: () => require('../commands/pull'),
|
|
44
|
+
link: () => require('../commands/link'),
|
|
45
|
+
login: () => require('../commands/login'),
|
|
46
|
+
ai: () => require('../commands/ai'),
|
|
47
|
+
music: () => require('../commands/music'),
|
|
48
|
+
sap: () => require('../commands/sap'),
|
|
44
49
|
help: showHelp,
|
|
45
50
|
version: showVersion,
|
|
46
51
|
};
|
|
@@ -54,17 +59,21 @@ function showHelp() {
|
|
|
54
59
|
showBrand();
|
|
55
60
|
console.log(` ${g('v' + pkg.version)} ยท ${g('phewsh.com')}\n`);
|
|
56
61
|
console.log(` ${b('Commands')}`);
|
|
57
|
-
console.log(` ${w('
|
|
58
|
-
console.log(` ${w('
|
|
62
|
+
console.log(` ${w('clarify')} Turn messy intent into a structured project spec`);
|
|
63
|
+
console.log(` ${w('push')} Push local .intent/ to cloud`);
|
|
64
|
+
console.log(` ${w('pull')} Pull project from cloud to .intent/`);
|
|
65
|
+
console.log(` ${w('link')} Link local .intent/ to a cloud project`);
|
|
66
|
+
console.log(` ${w('intent')} Manage .intent/ artifacts โ status, open, evolve`);
|
|
59
67
|
console.log(` ${w('ai')} Run context-aware AI prompts (reads .intent/)`);
|
|
68
|
+
console.log(` ${w('login')} Set up identity, API key, and cloud sync`);
|
|
60
69
|
console.log(` ${w('sap')} Sustainable AI Protocol โ usage and accountability`);
|
|
61
70
|
console.log(` ${w('music')} MBHD music engine`);
|
|
62
71
|
console.log('');
|
|
63
72
|
console.log(` ${b('Quick start')}`);
|
|
64
|
-
console.log(` ${g('phewsh login')}
|
|
65
|
-
console.log(` ${g('phewsh
|
|
66
|
-
console.log(` ${g('phewsh
|
|
67
|
-
console.log(` ${g('phewsh ai run "what\'s next?"')}
|
|
73
|
+
console.log(` ${g('phewsh login')} Set up identity + API key`);
|
|
74
|
+
console.log(` ${g('phewsh clarify')} Compile messy intent โ structured spec`);
|
|
75
|
+
console.log(` ${g('phewsh push')} Sync to cloud`);
|
|
76
|
+
console.log(` ${g('phewsh ai run "what\'s next?"')} AI with your project context`);
|
|
68
77
|
console.log('');
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// phewsh clarify
|
|
2
|
+
// Takes raw, messy intent and compiles it into a structured PPS.
|
|
3
|
+
// First run: creates .intent/ + pps.json + .md views.
|
|
4
|
+
// Subsequent runs: updates pps.json fields and regenerates views.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
const { readPPS, writePPS, createPPS, generateViews } = require('../lib/pps');
|
|
11
|
+
|
|
12
|
+
const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
13
|
+
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(3);
|
|
16
|
+
const textFlag = args.indexOf('--text');
|
|
17
|
+
const rawFromFlag = textFlag !== -1 ? args.slice(textFlag + 1).join(' ') : null;
|
|
18
|
+
const isUpdate = args.includes('--update') || args.includes('-u');
|
|
19
|
+
|
|
20
|
+
function loadConfig() {
|
|
21
|
+
if (!fs.existsSync(CONFIG_PATH)) return null;
|
|
22
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getProjectName() {
|
|
26
|
+
const existing = readPPS(INTENT_DIR);
|
|
27
|
+
if (existing?.entity) return existing.entity;
|
|
28
|
+
const visionPath = path.join(INTENT_DIR, 'vision.md');
|
|
29
|
+
if (fs.existsSync(visionPath)) {
|
|
30
|
+
const m = fs.readFileSync(visionPath, 'utf-8').match(/^entity:\s*(.+)$/m);
|
|
31
|
+
if (m) return m[1].trim();
|
|
32
|
+
}
|
|
33
|
+
return path.basename(process.cwd());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function askForInput() {
|
|
37
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
console.log('\n Describe what you\'re building. Be as messy as you want.\n');
|
|
40
|
+
console.log(' (You\'re the David Rose. PHEWSH is the business registration clerk.)\n');
|
|
41
|
+
process.stdout.write(' > ');
|
|
42
|
+
let input = '';
|
|
43
|
+
rl.on('line', (line) => { input += (input ? ' ' : '') + line.trim(); });
|
|
44
|
+
rl.on('close', () => resolve(input.trim()));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function callClarifyAPI(apiKey, raw, existing) {
|
|
49
|
+
const isRefine = !!(existing?.intent?.goal);
|
|
50
|
+
|
|
51
|
+
const systemPrompt = `You are a project compiler. Your job is to extract clean, structured intent from messy human input.
|
|
52
|
+
|
|
53
|
+
Return ONLY valid JSON โ no markdown, no explanation, no commentary. The JSON must match this exact schema:
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
"goal": "one sentence north star (what this is and why it exists)",
|
|
57
|
+
"success_criteria": ["measurable outcome", "measurable outcome"],
|
|
58
|
+
"constraints": ["constraint or non-negotiable", "..."],
|
|
59
|
+
"inputs": ["what this takes in or requires"],
|
|
60
|
+
"outputs": ["what this produces or delivers"],
|
|
61
|
+
"tasks": [
|
|
62
|
+
{"text": "first concrete action to take", "type": "do"},
|
|
63
|
+
{"text": "second action", "type": "do"}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Rules:
|
|
68
|
+
- goal: one sentence, no buzzwords, no hedging
|
|
69
|
+
- success_criteria: 2-5 items, must be measurable or observable
|
|
70
|
+
- constraints: real limits only (budget, time, technical, ethical). 0-3 items.
|
|
71
|
+
- inputs: what the project needs to function (data, people, tools). 1-4 items.
|
|
72
|
+
- outputs: what the project delivers. 1-4 items.
|
|
73
|
+
- tasks: 3-7 concrete next actions, specific enough to act on immediately
|
|
74
|
+
- type options: "do" (manual action), "copy" (command to run), "open" (URL to visit), "install" (package to install)
|
|
75
|
+
${isRefine ? '\nThis is a refinement of existing intent. The previous goal was: ' + existing.intent.goal : ''}`;
|
|
76
|
+
|
|
77
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'x-api-key': apiKey,
|
|
81
|
+
'anthropic-version': '2023-06-01',
|
|
82
|
+
'content-type': 'application/json',
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
model: 'claude-sonnet-4-6',
|
|
86
|
+
max_tokens: 1024,
|
|
87
|
+
system: systemPrompt,
|
|
88
|
+
messages: [{ role: 'user', content: raw }],
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const err = await response.json().catch(() => ({}));
|
|
94
|
+
throw new Error(err.error?.message || `API error ${response.status}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
const text = data.content?.[0]?.text || '';
|
|
99
|
+
|
|
100
|
+
// Strip markdown code fences if present
|
|
101
|
+
const clean = text.replace(/^```(?:json)?\n?/m, '').replace(/\n?```$/m, '').trim();
|
|
102
|
+
return JSON.parse(clean);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function writeViews(intentDir, pps) {
|
|
106
|
+
const { vision, plan, next } = generateViews(pps);
|
|
107
|
+
fs.writeFileSync(path.join(intentDir, 'vision.md'), vision);
|
|
108
|
+
fs.writeFileSync(path.join(intentDir, 'plan.md'), plan);
|
|
109
|
+
fs.writeFileSync(path.join(intentDir, 'next.md'), next);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
114
|
+
console.log(`
|
|
115
|
+
๐ฎโ๐จ๐คซ phewsh clarify
|
|
116
|
+
|
|
117
|
+
Usage:
|
|
118
|
+
phewsh clarify Interactive: describe your project, get structured PPS
|
|
119
|
+
phewsh clarify --text "..." Inline: pass raw text directly
|
|
120
|
+
phewsh clarify --update Refine existing PPS with new input
|
|
121
|
+
|
|
122
|
+
What it does:
|
|
123
|
+
Takes messy, buzzword-heavy input and compiles it into a structured project spec (PPS).
|
|
124
|
+
Writes .intent/pps.json as the source of truth.
|
|
125
|
+
Generates vision.md, plan.md, next.md as human-readable views.
|
|
126
|
+
|
|
127
|
+
Requires:
|
|
128
|
+
phewsh login --set-key Set your Anthropic API key first
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
phewsh clarify
|
|
132
|
+
phewsh clarify --text "I want to build a thing that helps people track habits with AI"
|
|
133
|
+
phewsh clarify --update After new context or direction change
|
|
134
|
+
`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const config = loadConfig();
|
|
139
|
+
if (!config?.apiKey) {
|
|
140
|
+
console.log('\n No API key found. Run `phewsh login --set-key` first.\n');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const existing = readPPS(INTENT_DIR);
|
|
145
|
+
if (existing && !isUpdate) {
|
|
146
|
+
console.log('\n .intent/pps.json already exists.');
|
|
147
|
+
console.log(' Run `phewsh clarify --update` to refine, or `phewsh intent --status` to view.\n');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('\n ๐ฎโ๐จ๐คซ phewsh clarify\n');
|
|
152
|
+
|
|
153
|
+
let raw = rawFromFlag;
|
|
154
|
+
if (!raw) {
|
|
155
|
+
if (!process.stdin.isTTY) {
|
|
156
|
+
console.error('\n Pipe input or use --text "your description"\n');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
raw = await askForInput();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!raw) {
|
|
163
|
+
console.log('\n Nothing to clarify.\n');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log('\n Compiling...\n');
|
|
168
|
+
|
|
169
|
+
let extracted;
|
|
170
|
+
try {
|
|
171
|
+
extracted = await callClarifyAPI(config.apiKey, raw, existing);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error('\n Clarify failed:', err.message, '\n');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const entity = getProjectName();
|
|
178
|
+
let pps;
|
|
179
|
+
|
|
180
|
+
if (existing && isUpdate) {
|
|
181
|
+
// Patch existing PPS with new intent fields, preserve id/created/tasks state
|
|
182
|
+
pps = {
|
|
183
|
+
...existing,
|
|
184
|
+
intent: {
|
|
185
|
+
raw,
|
|
186
|
+
goal: extracted.goal,
|
|
187
|
+
success_criteria: extracted.success_criteria || [],
|
|
188
|
+
constraints: extracted.constraints || [],
|
|
189
|
+
inputs: extracted.inputs || [],
|
|
190
|
+
outputs: extracted.outputs || [],
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
// Merge new tasks (preserve done tasks, add new ones)
|
|
194
|
+
const doneTasks = existing.tasks.filter(t => t.status === 'done');
|
|
195
|
+
const newTasks = (extracted.tasks || []).map((t, i) => ({
|
|
196
|
+
id: `t_${String(Date.now() + i).slice(-6)}`,
|
|
197
|
+
text: t.text,
|
|
198
|
+
status: 'open',
|
|
199
|
+
type: t.type || 'do',
|
|
200
|
+
blocked_by: null,
|
|
201
|
+
}));
|
|
202
|
+
pps.tasks = [...doneTasks, ...newTasks];
|
|
203
|
+
pps.state.phase = 'plan';
|
|
204
|
+
} else {
|
|
205
|
+
pps = createPPS({ entity, raw, intent: extracted });
|
|
206
|
+
pps.state.phase = 'plan';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fs.mkdirSync(INTENT_DIR, { recursive: true });
|
|
210
|
+
writePPS(INTENT_DIR, pps);
|
|
211
|
+
writeViews(INTENT_DIR, pps);
|
|
212
|
+
|
|
213
|
+
console.log(` โ .intent/pps.json โ structured project spec`);
|
|
214
|
+
console.log(` โ .intent/vision.md โ ${pps.intent.goal}`);
|
|
215
|
+
console.log(` โ .intent/plan.md โ ${pps.intent.success_criteria.length} outcomes, ${pps.intent.constraints.length} constraints`);
|
|
216
|
+
console.log(` โ .intent/next.md โ ${pps.tasks.length} actions\n`);
|
|
217
|
+
console.log(` Goal: ${pps.intent.goal}\n`);
|
|
218
|
+
if (pps.tasks.length > 0) {
|
|
219
|
+
console.log(' First actions:');
|
|
220
|
+
pps.tasks.slice(0, 3).forEach(t => console.log(` ยท ${t.text}`));
|
|
221
|
+
}
|
|
222
|
+
console.log(`
|
|
223
|
+
Next:
|
|
224
|
+
phewsh intent --status Review your artifacts
|
|
225
|
+
phewsh clarify --update Refine with more context
|
|
226
|
+
phewsh ai run "..." Run AI with this context
|
|
227
|
+
`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
main().catch(err => {
|
|
231
|
+
console.error('\n Error:', err.message);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
package/commands/intent.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const readline = require('readline');
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
|
+
const { createPPS, writePPS, generateViews } = require('../lib/pps');
|
|
5
6
|
|
|
6
7
|
const args = process.argv.slice(3);
|
|
7
8
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
@@ -113,97 +114,40 @@ async function initIntent() {
|
|
|
113
114
|
|
|
114
115
|
console.log('\n Creating .intent/ ...\n');
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
## Principles
|
|
135
|
-
<!-- Non-negotiable values and constraints. What will you never compromise on? -->
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
|
|
139
|
-
## Beneficiaries
|
|
140
|
-
<!-- Who benefits from this and how? Be specific. -->
|
|
141
|
-
`;
|
|
142
|
-
|
|
143
|
-
const plan = `---
|
|
144
|
-
entity: ${projectName}
|
|
145
|
-
archetype: product
|
|
146
|
-
created: ${date}
|
|
147
|
-
updated: ${date}
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
# Plan
|
|
151
|
-
|
|
152
|
-
## Current Strategy
|
|
153
|
-
<!-- One paragraph: the approach and why it's the right one right now. -->
|
|
154
|
-
|
|
155
|
-
## Systems
|
|
156
|
-
<!-- What needs to exist? Key components, tools, structures. -->
|
|
157
|
-
|
|
158
|
-
## Sequence
|
|
159
|
-
<!-- Phased plan. What must come first? What is blocked on what? -->
|
|
160
|
-
- Phase 1:
|
|
161
|
-
- Phase 2:
|
|
162
|
-
- Phase 3:
|
|
163
|
-
|
|
164
|
-
## Constraints
|
|
165
|
-
<!-- What limits this? Budget, time, team, technical. Be honest. -->
|
|
166
|
-
|
|
167
|
-
## Resources
|
|
168
|
-
<!-- What do you have available? Team, tools, existing assets. -->
|
|
169
|
-
`;
|
|
170
|
-
|
|
171
|
-
const next = `---
|
|
172
|
-
entity: ${projectName}
|
|
173
|
-
archetype: product
|
|
174
|
-
created: ${date}
|
|
175
|
-
updated: ${date}
|
|
176
|
-
---
|
|
177
|
-
|
|
178
|
-
# Next
|
|
179
|
-
|
|
180
|
-
## Current State
|
|
181
|
-
${what ? `Building: ${what}` : '<!-- Where things stand right now, honestly. -->'}
|
|
182
|
-
|
|
183
|
-
## Next Actions
|
|
184
|
-
- [ ] **Refine the vision** โ Open the web compass and complete vision.md
|
|
185
|
-
- [ ] **Define Phase 1** โ What is the smallest thing you can ship?
|
|
186
|
-
- [ ] **Identify the first blocker** โ What is standing between you and execution?
|
|
187
|
-
|
|
188
|
-
## Blocked
|
|
189
|
-
<!-- What is stuck and why? What decision is needed to unblock it? -->
|
|
190
|
-
|
|
191
|
-
## Metrics
|
|
192
|
-
<!-- 2-3 numbers that tell you if it's working. -->
|
|
193
|
-
`;
|
|
117
|
+
// Build a starter PPS and generate views from it
|
|
118
|
+
const pps = createPPS({
|
|
119
|
+
entity: projectName,
|
|
120
|
+
archetype: 'product',
|
|
121
|
+
raw: [what, goal].filter(Boolean).join(' '),
|
|
122
|
+
intent: {
|
|
123
|
+
goal: what || '',
|
|
124
|
+
success_criteria: goal ? [goal] : [],
|
|
125
|
+
constraints: [],
|
|
126
|
+
inputs: [],
|
|
127
|
+
outputs: [],
|
|
128
|
+
tasks: [
|
|
129
|
+
{ text: 'Refine the vision โ complete vision.md', type: 'do' },
|
|
130
|
+
{ text: 'Define Phase 1 โ what is the smallest thing to ship?', type: 'do' },
|
|
131
|
+
{ text: 'Identify the first blocker', type: 'do' },
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
});
|
|
194
135
|
|
|
136
|
+
writePPS(INTENT_DIR, pps);
|
|
137
|
+
const { vision, plan, next } = generateViews(pps);
|
|
195
138
|
fs.writeFileSync(path.join(INTENT_DIR, 'vision.md'), vision);
|
|
196
139
|
fs.writeFileSync(path.join(INTENT_DIR, 'plan.md'), plan);
|
|
197
140
|
fs.writeFileSync(path.join(INTENT_DIR, 'next.md'), next);
|
|
198
141
|
|
|
142
|
+
console.log(` โ .intent/pps.json โ Structured project spec (source of truth)`);
|
|
199
143
|
console.log(` โ .intent/vision.md โ The north star`);
|
|
200
144
|
console.log(` โ .intent/plan.md โ The strategy`);
|
|
201
145
|
console.log(` โ .intent/next.md โ What to do right now`);
|
|
202
146
|
console.log(`
|
|
203
|
-
|
|
204
|
-
Drop them in any AI coding session and your tools gain full understanding.
|
|
147
|
+
Tip: Run \`phewsh clarify\` to have AI compile your messy intent into a precise spec.
|
|
205
148
|
|
|
206
149
|
Next:
|
|
150
|
+
phewsh clarify Compile intent โ structured spec with AI
|
|
207
151
|
phewsh intent --open Open the web compass to go deeper
|
|
208
152
|
phewsh intent --status Check your progress any time
|
|
209
153
|
`);
|
package/commands/link.js
ADDED
package/commands/pull.js
ADDED
package/commands/push.js
ADDED
package/commands/sync.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const { select, upsert, refreshSession } = require('../lib/supabase');
|
|
9
|
+
const { readPPS, writePPS } = require('../lib/pps');
|
|
9
10
|
|
|
10
11
|
const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
11
12
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
@@ -69,18 +70,50 @@ async function push(config, token) {
|
|
|
69
70
|
token
|
|
70
71
|
);
|
|
71
72
|
|
|
73
|
+
// Read pps.json if it exists
|
|
74
|
+
const localPPS = readPPS(INTENT_DIR);
|
|
75
|
+
const archetype = localPPS?.archetype || 'product';
|
|
76
|
+
const projectId = localPPS?.adapters?.phewsh?.cloud_id || null;
|
|
77
|
+
|
|
72
78
|
if (existing.length > 0) {
|
|
73
79
|
project = existing[0];
|
|
74
|
-
} else {
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
} else if (projectId) {
|
|
81
|
+
// Linked to a specific cloud project โ fetch it
|
|
82
|
+
const linked = await select('projects', `id=eq.${projectId}&user_id=eq.${userId}&select=id,name`, token).catch(() => []);
|
|
83
|
+
project = linked[0] || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!project) {
|
|
87
|
+
const payload = {
|
|
88
|
+
id: (localPPS?.adapters?.phewsh?.cloud_id) || genProjectId(),
|
|
77
89
|
user_id: userId,
|
|
78
90
|
name: projectName,
|
|
79
|
-
archetype
|
|
80
|
-
freeform_text: '',
|
|
81
|
-
}
|
|
91
|
+
archetype,
|
|
92
|
+
freeform_text: localPPS?.intent?.raw || '',
|
|
93
|
+
};
|
|
94
|
+
if (localPPS) payload.pps_json = localPPS;
|
|
95
|
+
const created = await upsert('projects', payload, token);
|
|
82
96
|
project = Array.isArray(created) ? created[0] : created;
|
|
83
97
|
console.log(` Created project: ${projectName}`);
|
|
98
|
+
} else if (localPPS) {
|
|
99
|
+
// Update pps_json on existing project
|
|
100
|
+
await upsert('projects', {
|
|
101
|
+
id: project.id,
|
|
102
|
+
user_id: userId,
|
|
103
|
+
name: projectName,
|
|
104
|
+
archetype,
|
|
105
|
+
freeform_text: localPPS.intent?.raw || '',
|
|
106
|
+
pps_json: localPPS,
|
|
107
|
+
}, token).catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Store cloud project_id back into local pps.json for linking
|
|
111
|
+
if (localPPS && project?.id) {
|
|
112
|
+
if (!localPPS.adapters) localPPS.adapters = {};
|
|
113
|
+
if (!localPPS.adapters.phewsh) localPPS.adapters.phewsh = {};
|
|
114
|
+
localPPS.adapters.phewsh.cloud_id = project.id;
|
|
115
|
+
localPPS.adapters.phewsh.last_synced = new Date().toISOString();
|
|
116
|
+
writePPS(INTENT_DIR, localPPS);
|
|
84
117
|
}
|
|
85
118
|
|
|
86
119
|
// Push each artifact file
|
|
@@ -97,42 +130,51 @@ async function push(config, token) {
|
|
|
97
130
|
}, token);
|
|
98
131
|
pushed.push(file);
|
|
99
132
|
}
|
|
133
|
+
if (localPPS) pushed.unshift('pps.json');
|
|
100
134
|
|
|
101
|
-
console.log(`\n โ
|
|
135
|
+
console.log(`\n โ Pushed to cloud โ ${projectName} (${project.id})`);
|
|
102
136
|
pushed.forEach(f => console.log(` ${f}`));
|
|
103
137
|
console.log('');
|
|
104
138
|
}
|
|
105
139
|
|
|
106
|
-
async function pull(config, token) {
|
|
140
|
+
async function pull(config, token, cloudId = null) {
|
|
107
141
|
const projectName = getProjectName();
|
|
108
142
|
const userId = config.supabaseUserId;
|
|
109
143
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`name=eq.${encodeURIComponent(projectName)}&user_id=eq.${userId}&select=id,name
|
|
113
|
-
|
|
114
|
-
);
|
|
144
|
+
let query = cloudId
|
|
145
|
+
? `id=eq.${cloudId}&user_id=eq.${userId}&select=id,name,pps_json`
|
|
146
|
+
: `name=eq.${encodeURIComponent(projectName)}&user_id=eq.${userId}&select=id,name,pps_json`;
|
|
147
|
+
|
|
148
|
+
const projects = await select('projects', query, token);
|
|
115
149
|
|
|
116
150
|
if (projects.length === 0) {
|
|
117
|
-
console.log(`\n No cloud project found for "${projectName}".\n Push first with: phewsh
|
|
151
|
+
console.log(`\n No cloud project found for "${projectName}".\n Push first with: phewsh push\n`);
|
|
118
152
|
return;
|
|
119
153
|
}
|
|
120
154
|
|
|
121
155
|
const project = projects[0];
|
|
156
|
+
fs.mkdirSync(INTENT_DIR, { recursive: true });
|
|
157
|
+
|
|
158
|
+
const pulled = [];
|
|
159
|
+
|
|
160
|
+
// Restore pps.json from cloud if present
|
|
161
|
+
if (project.pps_json) {
|
|
162
|
+
const localPPS = readPPS(INTENT_DIR);
|
|
163
|
+
const merged = { ...project.pps_json };
|
|
164
|
+
// Keep any local adapter links
|
|
165
|
+
if (localPPS?.adapters) merged.adapters = { ...project.pps_json.adapters, ...localPPS.adapters };
|
|
166
|
+
merged.adapters = merged.adapters || {};
|
|
167
|
+
merged.adapters.phewsh = { cloud_id: project.id, last_synced: new Date().toISOString() };
|
|
168
|
+
writePPS(INTENT_DIR, merged);
|
|
169
|
+
pulled.push('pps.json');
|
|
170
|
+
}
|
|
171
|
+
|
|
122
172
|
const artifacts = await select(
|
|
123
173
|
'artifacts',
|
|
124
174
|
`project_id=eq.${project.id}&user_id=eq.${userId}&select=kind,content,updated_at`,
|
|
125
175
|
token
|
|
126
176
|
);
|
|
127
177
|
|
|
128
|
-
if (artifacts.length === 0) {
|
|
129
|
-
console.log('\n No artifacts found in cloud for this project.\n');
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
fs.mkdirSync(INTENT_DIR, { recursive: true });
|
|
134
|
-
|
|
135
|
-
const pulled = [];
|
|
136
178
|
for (const artifact of artifacts) {
|
|
137
179
|
const file = KIND_TO_FILE[artifact.kind];
|
|
138
180
|
if (!file) continue;
|
|
@@ -140,11 +182,43 @@ async function pull(config, token) {
|
|
|
140
182
|
pulled.push(file);
|
|
141
183
|
}
|
|
142
184
|
|
|
143
|
-
|
|
185
|
+
if (pulled.length === 0) {
|
|
186
|
+
console.log('\n No data found in cloud for this project.\n');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(`\n โ Pulled from cloud โ ${project.name} (${project.id})`);
|
|
144
191
|
pulled.forEach(f => console.log(` ${f}`));
|
|
145
192
|
console.log('');
|
|
146
193
|
}
|
|
147
194
|
|
|
195
|
+
async function link(config, token, cloudId) {
|
|
196
|
+
const projectName = getProjectName();
|
|
197
|
+
const userId = config.supabaseUserId;
|
|
198
|
+
|
|
199
|
+
const projects = await select('projects', `id=eq.${cloudId}&user_id=eq.${userId}&select=id,name`, token);
|
|
200
|
+
if (projects.length === 0) {
|
|
201
|
+
console.log(`\n No cloud project found with id: ${cloudId}\n`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const project = projects[0];
|
|
206
|
+
let localPPS = readPPS(INTENT_DIR);
|
|
207
|
+
if (!localPPS) {
|
|
208
|
+
console.log('\n No local .intent/pps.json found. Run `phewsh clarify` or `phewsh intent --init` first.\n');
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!localPPS.adapters) localPPS.adapters = {};
|
|
213
|
+
if (!localPPS.adapters.phewsh) localPPS.adapters.phewsh = {};
|
|
214
|
+
localPPS.adapters.phewsh.cloud_id = project.id;
|
|
215
|
+
localPPS.adapters.phewsh.last_synced = null;
|
|
216
|
+
writePPS(INTENT_DIR, localPPS);
|
|
217
|
+
|
|
218
|
+
console.log(`\n โ Linked .intent/ โ cloud project "${project.name}" (${project.id})\n`);
|
|
219
|
+
console.log(' Run `phewsh push` to sync.\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
148
222
|
async function main(direction = 'push') {
|
|
149
223
|
const config = loadConfig();
|
|
150
224
|
if (!config?.supabaseUserId) {
|
|
@@ -160,9 +234,17 @@ async function main(direction = 'push') {
|
|
|
160
234
|
|
|
161
235
|
if (direction === 'pull') {
|
|
162
236
|
await pull(config, token);
|
|
237
|
+
} else if (direction === 'link') {
|
|
238
|
+
const args = process.argv.slice(3);
|
|
239
|
+
const cloudId = args[1];
|
|
240
|
+
if (!cloudId) {
|
|
241
|
+
console.log('\n Usage: phewsh link <cloud-project-id>\n');
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
await link(config, token, cloudId);
|
|
163
245
|
} else {
|
|
164
246
|
await push(config, token);
|
|
165
247
|
}
|
|
166
248
|
}
|
|
167
249
|
|
|
168
|
-
module.exports = { main };
|
|
250
|
+
module.exports = { main, push, pull, link, ensureValidToken, loadConfig };
|
package/lib/pps.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// PPS โ Portable Project Spec
|
|
2
|
+
// pps.json is the source of truth. .md files are generated views.
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
function genId() {
|
|
9
|
+
return `p_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function genTaskId(index) {
|
|
13
|
+
return `t_${String(index + 1).padStart(3, '0')}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readPPS(intentDir) {
|
|
17
|
+
const p = path.join(intentDir, 'pps.json');
|
|
18
|
+
if (!fs.existsSync(p)) return null;
|
|
19
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch { return null; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writePPS(intentDir, data) {
|
|
23
|
+
fs.mkdirSync(intentDir, { recursive: true });
|
|
24
|
+
const now = new Date().toISOString().split('T')[0];
|
|
25
|
+
data.updated = now;
|
|
26
|
+
fs.writeFileSync(path.join(intentDir, 'pps.json'), JSON.stringify(data, null, 2));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createPPS({ entity, archetype = 'product', raw = '', intent = {} }) {
|
|
30
|
+
const now = new Date().toISOString().split('T')[0];
|
|
31
|
+
return {
|
|
32
|
+
id: genId(),
|
|
33
|
+
version: '0.1',
|
|
34
|
+
entity,
|
|
35
|
+
archetype,
|
|
36
|
+
created: now,
|
|
37
|
+
updated: now,
|
|
38
|
+
intent: {
|
|
39
|
+
raw,
|
|
40
|
+
goal: intent.goal || '',
|
|
41
|
+
success_criteria: intent.success_criteria || [],
|
|
42
|
+
constraints: intent.constraints || [],
|
|
43
|
+
inputs: intent.inputs || [],
|
|
44
|
+
outputs: intent.outputs || [],
|
|
45
|
+
},
|
|
46
|
+
tasks: (intent.tasks || []).map((t, i) => ({
|
|
47
|
+
id: genTaskId(i),
|
|
48
|
+
text: t.text,
|
|
49
|
+
status: 'open',
|
|
50
|
+
type: t.type || 'do',
|
|
51
|
+
blocked_by: null,
|
|
52
|
+
})),
|
|
53
|
+
state: {
|
|
54
|
+
phase: 'clarify',
|
|
55
|
+
last_action: now,
|
|
56
|
+
progress: 0,
|
|
57
|
+
},
|
|
58
|
+
assets: [],
|
|
59
|
+
adapters: {
|
|
60
|
+
anthropic: { project_id: null, last_synced: null },
|
|
61
|
+
openai: { project_id: null, last_synced: null },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function generateViews(pps) {
|
|
67
|
+
const { entity, intent, tasks, created, updated, archetype } = pps;
|
|
68
|
+
|
|
69
|
+
const vision = `---
|
|
70
|
+
entity: ${entity}
|
|
71
|
+
archetype: ${archetype}
|
|
72
|
+
created: ${created}
|
|
73
|
+
updated: ${updated}
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
# Vision
|
|
77
|
+
|
|
78
|
+
## North Star
|
|
79
|
+
${intent.goal || `What is ${entity} and why does it exist?`}
|
|
80
|
+
|
|
81
|
+
## Outcomes
|
|
82
|
+
${intent.success_criteria.length > 0
|
|
83
|
+
? intent.success_criteria.map(c => `- ${c}`).join('\n')
|
|
84
|
+
: '<!-- What does success look like? 3-5 concrete outcomes. -->'}
|
|
85
|
+
|
|
86
|
+
## Principles
|
|
87
|
+
${intent.constraints.length > 0
|
|
88
|
+
? intent.constraints.map(c => `- ${c}`).join('\n')
|
|
89
|
+
: '<!-- Non-negotiable values and constraints. -->'}
|
|
90
|
+
|
|
91
|
+
## Beneficiaries
|
|
92
|
+
<!-- Who benefits from this and how? -->
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const plan = `---
|
|
96
|
+
entity: ${entity}
|
|
97
|
+
archetype: ${archetype}
|
|
98
|
+
created: ${created}
|
|
99
|
+
updated: ${updated}
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
# Plan
|
|
103
|
+
|
|
104
|
+
## Current Strategy
|
|
105
|
+
<!-- One paragraph: the approach and why it's the right one right now. -->
|
|
106
|
+
|
|
107
|
+
## Systems
|
|
108
|
+
${intent.inputs.length > 0 || intent.outputs.length > 0
|
|
109
|
+
? [
|
|
110
|
+
intent.inputs.length > 0 ? `**Inputs:** ${intent.inputs.join(', ')}` : '',
|
|
111
|
+
intent.outputs.length > 0 ? `**Outputs:** ${intent.outputs.join(', ')}` : '',
|
|
112
|
+
].filter(Boolean).join('\n')
|
|
113
|
+
: '<!-- Key components, tools, structures. -->'}
|
|
114
|
+
|
|
115
|
+
## Sequence
|
|
116
|
+
${tasks.length > 0
|
|
117
|
+
? tasks.slice(0, 5).map((t, i) => `- Phase ${i + 1}: ${t.text}`).join('\n')
|
|
118
|
+
: '- Phase 1:\n- Phase 2:\n- Phase 3:'}
|
|
119
|
+
|
|
120
|
+
## Constraints
|
|
121
|
+
${intent.constraints.length > 0
|
|
122
|
+
? intent.constraints.map(c => `- ${c}`).join('\n')
|
|
123
|
+
: '<!-- What limits this? Budget, time, team, technical. -->'}
|
|
124
|
+
|
|
125
|
+
## Resources
|
|
126
|
+
<!-- What do you have available? Team, tools, existing assets. -->
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
const next = `---
|
|
130
|
+
entity: ${entity}
|
|
131
|
+
archetype: ${archetype}
|
|
132
|
+
created: ${created}
|
|
133
|
+
updated: ${updated}
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
# Next
|
|
137
|
+
|
|
138
|
+
## Current State
|
|
139
|
+
${intent.goal ? `Building: ${intent.goal}` : '<!-- Where things stand right now. -->'}
|
|
140
|
+
|
|
141
|
+
## Next Actions
|
|
142
|
+
${tasks.length > 0
|
|
143
|
+
? tasks.map(t => `- [ ] **${t.text}**`).join('\n')
|
|
144
|
+
: `- [ ] **Refine the vision** โ Open the web compass and complete vision.md
|
|
145
|
+
- [ ] **Define Phase 1** โ What is the smallest thing you can ship?
|
|
146
|
+
- [ ] **Identify the first blocker** โ What is standing between you and execution?`}
|
|
147
|
+
|
|
148
|
+
## Blocked
|
|
149
|
+
<!-- What is stuck and why? -->
|
|
150
|
+
|
|
151
|
+
## Metrics
|
|
152
|
+
<!-- 2-3 numbers that tell you if it's working. -->
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
return { vision, plan, next };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { readPPS, writePPS, createPPS, generateViews, genId };
|