create-sl-ide 0.0.0-stage → 0.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.
- package/README.md +48 -2
- package/index.mjs +271 -0
- package/lib/cloud.mjs +92 -0
- package/lib/run.mjs +23 -0
- package/package.json +33 -4
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-sl-ide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Start a slide deck with [sl-ide](https://github.com/dberm312/sl-ide-template),
|
|
4
|
+
the IDE for building slides with Claude Code or Codex.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pnpm create sl-ide my-deck
|
|
8
|
+
# or: npm create sl-ide@latest my-deck · bun create sl-ide my-deck
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It asks a few questions, then:
|
|
12
|
+
|
|
13
|
+
1. Clones the **latest** sl-ide template from GitHub, keeping its history as
|
|
14
|
+
the `upstream` remote, so `git pull upstream main` brings later editor
|
|
15
|
+
updates into your deck. The editor isn't bundled in this package, so every
|
|
16
|
+
new deck starts from the template's current `main`.
|
|
17
|
+
2. Creates the deck's own GitHub repo (private by default).
|
|
18
|
+
3. Sets it up to publish to [sl-ide cloud](https://sl-ide.com): signs you in
|
|
19
|
+
through your browser, saves a token for your team, writes
|
|
20
|
+
`sl-ide.config.json` and sets the repo's `SL_IDE_PUBLISH_TOKEN` secret.
|
|
21
|
+
Every push to `main` then updates `sl-ide.com/d/my-deck/`.
|
|
22
|
+
4. Runs `pnpm install`.
|
|
23
|
+
|
|
24
|
+
Then `cd my-deck && pnpm dev`, open Claude Code or Codex there, and run
|
|
25
|
+
`/new-deck`.
|
|
26
|
+
|
|
27
|
+
## Options
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
--dir <path> folder for the deck (default ./<name>)
|
|
31
|
+
--github <private|public|none> the deck's repo (default private)
|
|
32
|
+
--team <slug> the sl-ide cloud team that owns the deck
|
|
33
|
+
--share <team|public> who can open its link (default team)
|
|
34
|
+
--no-publish skip sl-ide cloud
|
|
35
|
+
--no-install skip pnpm install
|
|
36
|
+
--no-open print the sign-in link instead of opening it
|
|
37
|
+
-y, --yes take the defaults instead of asking
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
pnpm create sl-ide login # sign in and save a token for a team
|
|
42
|
+
pnpm create sl-ide logout # forget saved tokens
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Tokens are saved in `~/.config/sl-ide/credentials.json`, readable only by
|
|
46
|
+
you, and can be revoked at sl-ide.com/tokens.
|
|
47
|
+
|
|
48
|
+
Needs Node 20.12+, git and pnpm, plus the GitHub CLI (`gh`) for the repo
|
|
49
|
+
and publishing.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pnpm create sl-ide [name] [options] (also npm create sl-ide, bunx create-sl-ide)
|
|
3
|
+
//
|
|
4
|
+
// Starts a new deck: clones the latest sl-ide template from GitHub (with its
|
|
5
|
+
// history, as `upstream`, so `git pull upstream main` brings later editor
|
|
6
|
+
// updates), creates the deck's own repo, sets it up to publish to sl-ide
|
|
7
|
+
// cloud, and installs dependencies. The editor itself is never bundled here,
|
|
8
|
+
// so a new deck always starts from the template's main.
|
|
9
|
+
//
|
|
10
|
+
// pnpm create sl-ide login sign in to sl-ide cloud and save a team token
|
|
11
|
+
// pnpm create sl-ide logout forget saved tokens
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { parseArgs } from 'node:util';
|
|
15
|
+
import * as p from '@clack/prompts';
|
|
16
|
+
import { CLOUD, credsPath, deviceLogin, forget, legacyToken, readCreds, saveToken, slugAvailable, whoami } from './lib/cloud.mjs';
|
|
17
|
+
import { ghReady, has, run } from './lib/run.mjs';
|
|
18
|
+
|
|
19
|
+
const TEMPLATE = process.env.SL_IDE_TEMPLATE || 'https://github.com/dberm312/sl-ide-template.git';
|
|
20
|
+
const SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
21
|
+
|
|
22
|
+
const HELP = `
|
|
23
|
+
pnpm create sl-ide [name] [options]
|
|
24
|
+
|
|
25
|
+
Start a new deck from the latest sl-ide template.
|
|
26
|
+
|
|
27
|
+
Options
|
|
28
|
+
--dir <path> folder to create the deck in (default: ./<name>)
|
|
29
|
+
--github <private|public|none> the deck's GitHub repo (default: private)
|
|
30
|
+
--team <slug> the sl-ide cloud team that owns the deck
|
|
31
|
+
--share <team|public> who can open its link (default: team)
|
|
32
|
+
--no-publish don't set up publishing to sl-ide cloud
|
|
33
|
+
--no-install skip pnpm install
|
|
34
|
+
--no-open print the sign-in link instead of opening a browser
|
|
35
|
+
-y, --yes take the defaults instead of asking
|
|
36
|
+
-h, --help
|
|
37
|
+
|
|
38
|
+
Commands
|
|
39
|
+
login sign in to sl-ide cloud and save a team token
|
|
40
|
+
logout [team] forget saved tokens
|
|
41
|
+
|
|
42
|
+
Every push to the deck's repo publishes it to ${CLOUD}/d/<name>/.
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const { values: o, positionals } = parseArgs({
|
|
46
|
+
allowPositionals: true,
|
|
47
|
+
options: {
|
|
48
|
+
dir: { type: 'string' },
|
|
49
|
+
github: { type: 'string' },
|
|
50
|
+
team: { type: 'string' },
|
|
51
|
+
share: { type: 'string' },
|
|
52
|
+
publish: { type: 'boolean', default: true },
|
|
53
|
+
'no-publish': { type: 'boolean' },
|
|
54
|
+
install: { type: 'boolean', default: true },
|
|
55
|
+
'no-install': { type: 'boolean' },
|
|
56
|
+
open: { type: 'boolean', default: true },
|
|
57
|
+
'no-open': { type: 'boolean' },
|
|
58
|
+
yes: { type: 'boolean', short: 'y' },
|
|
59
|
+
help: { type: 'boolean', short: 'h' },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (o['no-publish']) o.publish = false;
|
|
63
|
+
if (o['no-install']) o.install = false;
|
|
64
|
+
if (o['no-open']) o.open = false;
|
|
65
|
+
if (o.help) { console.log(HELP); process.exit(0); }
|
|
66
|
+
|
|
67
|
+
const interactive = process.stdin.isTTY && !o.yes;
|
|
68
|
+
const bail = (msg) => { p.cancel(msg); process.exit(1); };
|
|
69
|
+
const ask = async (prompt) => {
|
|
70
|
+
const v = await prompt;
|
|
71
|
+
if (p.isCancel(v)) bail('Cancelled.');
|
|
72
|
+
return v;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// --- sign-in ---------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
async function signIn() {
|
|
78
|
+
const s = p.spinner();
|
|
79
|
+
const got = await deviceLogin({
|
|
80
|
+
open: o.open,
|
|
81
|
+
onCode: (code, url) => {
|
|
82
|
+
p.note(`${url}\n\nCode: ${code}`, o.open ? 'Approve in your browser' : 'Open this link to approve');
|
|
83
|
+
s.start('Waiting for approval');
|
|
84
|
+
},
|
|
85
|
+
}).catch((e) => { s.stop('Sign-in failed'); bail(e.message); });
|
|
86
|
+
s.stop(`Signed in for team ${got.team}`);
|
|
87
|
+
saveToken(got.team, got.token);
|
|
88
|
+
return got;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A token for the deck's team: a saved one, the legacy admin token, or a new
|
|
92
|
+
// browser sign-in. Returns { team, token } or null to skip publishing.
|
|
93
|
+
async function publishCredentials() {
|
|
94
|
+
const saved = readCreds().teams;
|
|
95
|
+
const teams = Object.keys(saved);
|
|
96
|
+
const admin = legacyToken();
|
|
97
|
+
|
|
98
|
+
if (o.team) {
|
|
99
|
+
if (!SLUG_RE.test(o.team)) bail(`--team "${o.team}" isn't a team slug`);
|
|
100
|
+
if (saved[o.team]) return { team: o.team, token: saved[o.team].token };
|
|
101
|
+
if (admin) return { team: o.team, token: admin };
|
|
102
|
+
p.log.info(`No saved token for team ${o.team}. Sign in to make one.`);
|
|
103
|
+
const got = await signIn();
|
|
104
|
+
if (got.team !== o.team) p.log.warn(`You approved team ${got.team}; publishing for that team instead.`);
|
|
105
|
+
return got;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!interactive) {
|
|
109
|
+
if (teams.length === 1) return { team: teams[0], token: saved[teams[0]].token };
|
|
110
|
+
p.log.warn('Not publishing: pass --team <slug>, or run pnpm create sl-ide login first.');
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const choice = await ask(p.select({
|
|
115
|
+
message: 'Publish this deck to sl-ide cloud?',
|
|
116
|
+
options: [
|
|
117
|
+
...teams.map((t) => ({ value: `team:${t}`, label: `Yes, for team ${t}` })),
|
|
118
|
+
{ value: 'login', label: teams.length ? 'Yes, for another team (sign in)' : 'Yes (sign in to sl-ide cloud)', hint: 'opens your browser' },
|
|
119
|
+
{ value: 'skip', label: 'Not now', hint: 'add sl-ide.config.json later' },
|
|
120
|
+
],
|
|
121
|
+
}));
|
|
122
|
+
if (choice === 'skip') return null;
|
|
123
|
+
if (choice === 'login') return signIn();
|
|
124
|
+
const team = choice.slice(5);
|
|
125
|
+
return { team, token: saved[team].token };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- commands --------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
async function login() {
|
|
131
|
+
p.intro('sl-ide cloud');
|
|
132
|
+
await signIn();
|
|
133
|
+
p.outro(`Saved to ${credsPath}. New decks can publish for this team.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function logout() {
|
|
137
|
+
forget(positionals[1]);
|
|
138
|
+
console.log(positionals[1] ? `Forgot the token for ${positionals[1]}.` : 'Forgot all saved sl-ide tokens.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function create() {
|
|
142
|
+
p.intro('sl-ide · new deck');
|
|
143
|
+
|
|
144
|
+
let name = positionals[0];
|
|
145
|
+
if (!name) {
|
|
146
|
+
if (!interactive) bail('Give the deck a name: pnpm create sl-ide my-deck');
|
|
147
|
+
name = await ask(p.text({
|
|
148
|
+
message: 'Deck name',
|
|
149
|
+
placeholder: 'acme-pitch',
|
|
150
|
+
validate: (v) => (/^[A-Za-z0-9._-]+$/.test(v ?? '') ? undefined : 'Letters, digits, . _ and - only'),
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) bail(`"${name}" isn't a valid name (letters, digits, . _ -)`);
|
|
154
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
155
|
+
if (!SLUG_RE.test(slug)) bail(`can't make a link name from "${name}"`);
|
|
156
|
+
|
|
157
|
+
const dest = path.resolve(o.dir ?? name);
|
|
158
|
+
if (fs.existsSync(dest) && fs.readdirSync(dest).length) bail(`${dest} already exists and isn't empty`);
|
|
159
|
+
|
|
160
|
+
for (const tool of ['git', 'pnpm']) {
|
|
161
|
+
if (!has(tool)) bail(tool === 'pnpm' ? 'pnpm is needed: corepack enable pnpm' : 'git is needed');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// GitHub repo
|
|
165
|
+
let github = o.github;
|
|
166
|
+
const gh = ghReady();
|
|
167
|
+
if (!github) {
|
|
168
|
+
github = !gh ? 'none'
|
|
169
|
+
: interactive ? await ask(p.select({
|
|
170
|
+
message: 'GitHub repo for the deck',
|
|
171
|
+
options: [
|
|
172
|
+
{ value: 'private', label: 'Private', hint: 'recommended' },
|
|
173
|
+
{ value: 'public', label: 'Public' },
|
|
174
|
+
{ value: 'none', label: 'None for now' },
|
|
175
|
+
],
|
|
176
|
+
})) : 'private';
|
|
177
|
+
if (!gh) p.log.warn('GitHub CLI not signed in (gh auth login), so no repo will be made. Publishing needs one.');
|
|
178
|
+
}
|
|
179
|
+
if (!['private', 'public', 'none'].includes(github)) bail('--github is private, public or none');
|
|
180
|
+
if (github !== 'none' && !gh) bail('gh is not installed or not signed in: gh auth login');
|
|
181
|
+
let owner = null;
|
|
182
|
+
if (github !== 'none') {
|
|
183
|
+
owner = await run('gh', ['api', 'user', '-q', '.login']);
|
|
184
|
+
const exists = await run('gh', ['repo', 'view', `${owner}/${name}`]).then(() => true, () => false);
|
|
185
|
+
if (exists) bail(`github.com/${owner}/${name} already exists`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Publishing
|
|
189
|
+
let pub = null;
|
|
190
|
+
let share = o.share ?? 'team';
|
|
191
|
+
if (o.publish && github !== 'none') {
|
|
192
|
+
pub = await publishCredentials();
|
|
193
|
+
if (pub) {
|
|
194
|
+
const ok = await whoami(pub.token);
|
|
195
|
+
if (!ok) bail(`The saved token for ${pub.team} was rejected (revoked?). Run: pnpm create sl-ide login`);
|
|
196
|
+
const avail = await slugAvailable(pub.token, slug);
|
|
197
|
+
if (!avail.available) bail(`Can't publish as ${slug}: ${avail.reason}. Pick another name.`);
|
|
198
|
+
if (avail.exists) p.log.warn(`Team ${pub.team} already has a deck called ${slug}; this one will publish over it.`);
|
|
199
|
+
if (!o.share && interactive) {
|
|
200
|
+
share = await ask(p.select({
|
|
201
|
+
message: 'Who can open its link?',
|
|
202
|
+
options: [
|
|
203
|
+
{ value: 'team', label: `Team ${pub.team} only`, hint: 'signed in' },
|
|
204
|
+
{ value: 'public', label: 'Anyone with the link' },
|
|
205
|
+
],
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
if (!['team', 'public'].includes(share)) bail('--share is team or public');
|
|
209
|
+
}
|
|
210
|
+
} else if (o.publish && github === 'none') {
|
|
211
|
+
p.log.info('Publishing runs in GitHub Actions, so it is off without a repo.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Do it
|
|
215
|
+
const s = p.spinner();
|
|
216
|
+
try {
|
|
217
|
+
s.start('Cloning the latest sl-ide template');
|
|
218
|
+
await run('git', ['clone', '-q', TEMPLATE, dest]);
|
|
219
|
+
await run('git', ['remote', 'rename', 'origin', 'upstream'], { cwd: dest });
|
|
220
|
+
await run('git', ['config', 'pull.rebase', 'false'], { cwd: dest });
|
|
221
|
+
const head = await run('git', ['log', '-1', '--format=%h %s'], { cwd: dest });
|
|
222
|
+
s.stop(`Cloned sl-ide (${head.split(' ')[0]})`);
|
|
223
|
+
|
|
224
|
+
if (pub) {
|
|
225
|
+
const config = { slug, team: pub.team, visibility: share };
|
|
226
|
+
fs.writeFileSync(path.join(dest, 'sl-ide.config.json'), `${JSON.stringify(config, null, 2)}\n`);
|
|
227
|
+
await run('git', ['add', 'sl-ide.config.json'], { cwd: dest });
|
|
228
|
+
await run('git', ['-c', 'core.hooksPath=/dev/null', 'commit', '-q', '-m', `Publish to sl-ide cloud as ${slug} (${share}, team ${pub.team})`], { cwd: dest });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (github !== 'none') {
|
|
232
|
+
s.start(`Creating github.com/${owner}/${name}`);
|
|
233
|
+
await run('gh', ['repo', 'create', name, `--${github}`, '--source', '.', '--remote', 'origin'], { cwd: dest });
|
|
234
|
+
// Before the first push, so its CI run publishes.
|
|
235
|
+
if (pub) await run('gh', ['secret', 'set', 'SL_IDE_PUBLISH_TOKEN', '--repo', `${owner}/${name}`], { cwd: dest, input: pub.token });
|
|
236
|
+
await run('git', ['push', '-q', '-u', 'origin', 'main'], { cwd: dest });
|
|
237
|
+
s.stop(`Created github.com/${owner}/${name} (${github})`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (o.install) {
|
|
241
|
+
s.start('Installing dependencies');
|
|
242
|
+
await run('pnpm', ['install', '--silent'], { cwd: dest });
|
|
243
|
+
s.stop('Installed dependencies');
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
s.stop('Stopped');
|
|
247
|
+
bail(e.message);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const rel = path.relative(process.cwd(), dest) || '.';
|
|
251
|
+
const lines = [
|
|
252
|
+
`cd ${rel}`,
|
|
253
|
+
...(o.install ? [] : ['pnpm install']),
|
|
254
|
+
'pnpm dev',
|
|
255
|
+
'',
|
|
256
|
+
'Then open Claude Code or Codex in that folder and run /new-deck',
|
|
257
|
+
'(Codex: $new-deck) to learn your style and outline the story.',
|
|
258
|
+
];
|
|
259
|
+
p.note(lines.join('\n'), 'Next');
|
|
260
|
+
if (pub) {
|
|
261
|
+
p.log.success(`Every push to main publishes ${CLOUD}/d/${slug}/ (${share === 'public' ? 'anyone with the link' : `team ${pub.team}`}).`);
|
|
262
|
+
} else {
|
|
263
|
+
p.log.info('Not publishing. To publish later, add sl-ide.config.json (see the deck\'s README).');
|
|
264
|
+
}
|
|
265
|
+
p.outro('Deck ready.');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const cmd = positionals[0];
|
|
269
|
+
if (cmd === 'login') await login();
|
|
270
|
+
else if (cmd === 'logout') logout();
|
|
271
|
+
else await create();
|
package/lib/cloud.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// sl-ide cloud: saved credentials, browser sign-in, and the API calls the
|
|
2
|
+
// create flow needs. Credentials live in ~/.config/sl-ide/credentials.json
|
|
3
|
+
// (mode 600): one publish token per team.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
export const CLOUD = (process.env.SL_IDE_URL || 'https://sl-ide.com').replace(/\/$/, '');
|
|
10
|
+
const CONF = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'sl-ide');
|
|
11
|
+
const CREDS = path.join(CONF, 'credentials.json');
|
|
12
|
+
const LEGACY = path.join(CONF, 'publish-token');
|
|
13
|
+
|
|
14
|
+
export function readCreds() {
|
|
15
|
+
try {
|
|
16
|
+
const c = JSON.parse(fs.readFileSync(CREDS, 'utf8'));
|
|
17
|
+
return { teams: {}, ...c };
|
|
18
|
+
} catch {
|
|
19
|
+
return { teams: {} };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function saveToken(team, token) {
|
|
24
|
+
const c = readCreds();
|
|
25
|
+
c.url = CLOUD;
|
|
26
|
+
c.teams[team] = { token, savedAt: new Date().toISOString() };
|
|
27
|
+
fs.mkdirSync(CONF, { recursive: true });
|
|
28
|
+
fs.writeFileSync(CREDS, `${JSON.stringify(c, null, 2)}\n`, { mode: 0o600 });
|
|
29
|
+
fs.chmodSync(CREDS, 0o600);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function forget(team) {
|
|
33
|
+
const c = readCreds();
|
|
34
|
+
if (team) delete c.teams[team];
|
|
35
|
+
else c.teams = {};
|
|
36
|
+
if (fs.existsSync(CREDS)) fs.writeFileSync(CREDS, `${JSON.stringify(c, null, 2)}\n`, { mode: 0o600 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// An admin token from before team tokens existed: publishes for any team.
|
|
40
|
+
export function legacyToken() {
|
|
41
|
+
try { return fs.readFileSync(LEGACY, 'utf8').trim() || null; } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const credsPath = CREDS;
|
|
45
|
+
|
|
46
|
+
async function api(pathname, { token, method = 'GET', body } = {}) {
|
|
47
|
+
const res = await fetch(`${CLOUD}${pathname}`, {
|
|
48
|
+
method,
|
|
49
|
+
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body ? { 'Content-Type': 'application/json' } : {}) },
|
|
50
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
51
|
+
});
|
|
52
|
+
const json = await res.json().catch(() => ({}));
|
|
53
|
+
return { status: res.status, ok: res.ok, json };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Is the saved token still good? -> { team } or null.
|
|
57
|
+
export async function whoami(token) {
|
|
58
|
+
const r = await api('/api/cli/whoami', { token }).catch(() => null);
|
|
59
|
+
return r?.ok ? r.json : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function slugAvailable(token, slug) {
|
|
63
|
+
const r = await api(`/api/cli/slug/${slug}`, { token });
|
|
64
|
+
return r.ok ? r.json : { available: false, reason: r.json.error || `HTTP ${r.status}` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function openBrowser(url) {
|
|
68
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
69
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
70
|
+
try {
|
|
71
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => {}).unref();
|
|
72
|
+
} catch { /* no browser: the URL is printed anyway */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Browser sign-in. onCode(user_code, url) is called once, to show the code;
|
|
76
|
+
// resolves { team, token } once a team member approves it at sl-ide.com/cli.
|
|
77
|
+
export async function deviceLogin({ onCode, open = true }) {
|
|
78
|
+
const start = await api('/api/cli/login/start', { method: 'POST', body: { client: os.hostname().replace(/\.local$/, '') } });
|
|
79
|
+
if (!start.ok) throw new Error(`could not reach ${CLOUD} (${start.status})`);
|
|
80
|
+
const { device_code: device, user_code: code, verification_uri_complete: url, interval = 2, expires_in: ttl = 600 } = start.json;
|
|
81
|
+
onCode(code, url);
|
|
82
|
+
if (open) openBrowser(url);
|
|
83
|
+
const until = Date.now() + ttl * 1000;
|
|
84
|
+
while (Date.now() < until) {
|
|
85
|
+
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
86
|
+
const r = await api('/api/cli/login/poll', { method: 'POST', body: { device_code: device } }).catch(() => null);
|
|
87
|
+
if (!r || r.status === 202) continue;
|
|
88
|
+
if (r.ok && r.json.token) return { team: r.json.team, token: r.json.token };
|
|
89
|
+
throw new Error('the sign-in code expired; run the command again');
|
|
90
|
+
}
|
|
91
|
+
throw new Error('the sign-in code expired; run the command again');
|
|
92
|
+
}
|
package/lib/run.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
// Run a command; resolve its stdout, reject with its stderr.
|
|
4
|
+
export function run(cmd, args, { cwd, input, quiet = true } = {}) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const p = spawn(cmd, args, { cwd, stdio: [input == null ? 'ignore' : 'pipe', 'pipe', 'pipe'] });
|
|
7
|
+
let out = '';
|
|
8
|
+
let err = '';
|
|
9
|
+
p.stdout.on('data', (d) => { out += d; if (!quiet) process.stdout.write(d); });
|
|
10
|
+
p.stderr.on('data', (d) => { err += d; });
|
|
11
|
+
if (input != null) { p.stdin.write(input); p.stdin.end(); }
|
|
12
|
+
p.on('error', reject);
|
|
13
|
+
p.on('close', (code) => (code === 0 ? resolve(out.trim()) : reject(new Error(`${cmd} ${args.join(' ')}: ${err.trim() || `exit ${code}`}`))));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function has(cmd) {
|
|
18
|
+
return spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], { stdio: 'ignore' }).status === 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ghReady() {
|
|
22
|
+
return has('gh') && spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }).status === 0;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-sl-ide",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Start a slide deck with sl-ide: pnpm create sl-ide",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-sl-ide": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"lib/"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.12"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"sl-ide",
|
|
18
|
+
"slides",
|
|
19
|
+
"deck",
|
|
20
|
+
"presentation",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"codex",
|
|
23
|
+
"create"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://sl-ide.com",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/dberm312/create-sl-ide.git"
|
|
29
|
+
},
|
|
30
|
+
"author": "David Berman",
|
|
31
|
+
"packageManager": "pnpm@12.11.2",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@clack/prompts": "^1.8.1"
|
|
34
|
+
}
|
|
35
|
+
}
|